Skip to content

Commit

Permalink
Implement the hitcount app
Browse files Browse the repository at this point in the history
  • Loading branch information
etchegom committed Jan 16, 2025
1 parent 4ea77ff commit 4b5f88f
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 0 deletions.
27 changes: 27 additions & 0 deletions recoco/apps/hitcount/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.contrib import admin

from .models import Hit, HitCount


@admin.register(HitCount)
class HitCountAdmin(admin.ModelAdmin):
list_display = (
"site",
"content_object",
"context_object",
"created",
)
list_filter = (
"site",
"created",
)


@admin.register(Hit)
class HitAdmin(admin.ModelAdmin):
list_display = (
"user",
"hitcount",
"created",
)
list_filter = ("created",)
6 changes: 6 additions & 0 deletions recoco/apps/hitcount/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class HitcountConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "recoco.apps.hitcount"
149 changes: 149 additions & 0 deletions recoco/apps/hitcount/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Generated by Django 4.2.17 on 2025-01-08 08:12

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields


class Migration(migrations.Migration):

initial = True

dependencies = [
("sites", "0002_alter_domain_unique"),
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="HitCount",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
("content_object_id", models.PositiveIntegerField()),
(
"context_object_id",
models.PositiveIntegerField(blank=True, null=True),
),
(
"content_ct",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hitcount_set_for_content",
to="contenttypes.contenttype",
),
),
(
"context_ct",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="hitcount_set_for_context",
to="contenttypes.contenttype",
),
),
(
"site",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="sites.site"
),
),
],
options={
"verbose_name": "hit count",
"verbose_name_plural": "hit counts",
"ordering": ("-created",),
"get_latest_by": "modified",
"unique_together": {
(
"site",
"content_ct",
"content_object_id",
"context_ct",
"context_object_id",
)
},
},
),
migrations.CreateModel(
name="Hit",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
("user_agent", models.CharField(editable=False, max_length=255)),
(
"hitcount",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
related_name="hits",
to="hitcount.hitcount",
),
),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "hit",
"verbose_name_plural": "hits",
"ordering": ("-created",),
"get_latest_by": "created",
},
),
]
Empty file.
56 changes: 56 additions & 0 deletions recoco/apps/hitcount/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.db import models
from django.utils.translation import gettext_lazy as _
from model_utils.models import TimeStampedModel


class HitCount(TimeStampedModel):
site = models.ForeignKey(Site, on_delete=models.CASCADE)

content_ct = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="hitcount_set_for_content",
)
content_object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_ct", "content_object_id")

context_ct = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="hitcount_set_for_context",
null=True,
blank=True,
)
context_object_id = models.PositiveIntegerField(null=True, blank=True)
context_object = GenericForeignKey("context_ct", "context_object_id")

class Meta:
verbose_name = _("hit count")
verbose_name_plural = _("hit counts")
ordering = ("-created",)
get_latest_by = "modified"
unique_together = (
"site",
"content_ct",
"content_object_id",
"context_ct",
"context_object_id",
)


class Hit(TimeStampedModel):
user_agent = models.CharField(max_length=255, editable=False)
user = models.ForeignKey(User, editable=False, on_delete=models.CASCADE)
hitcount = models.ForeignKey(
HitCount, editable=False, on_delete=models.CASCADE, related_name="hits"
)

class Meta:
verbose_name = _("hit")
verbose_name_plural = _("hits")
ordering = ("-created",)
get_latest_by = "created"
59 changes: 59 additions & 0 deletions recoco/apps/hitcount/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Any

from django.apps import apps
from rest_framework import serializers

from .utils import ct_from_label


class HitInputSerializer(serializers.Serializer):
content_ct = serializers.CharField()
content_object_id = serializers.IntegerField()
context_ct = serializers.CharField(required=False)
context_object_id = serializers.IntegerField(required=False)

def validate(self, data):
content_ct = data.get("content_ct")
content_object_id = data.get("content_object_id")
context_ct = data.get("context_ct")
context_object_id = data.get("context_object_id")

if context_ct and not context_object_id:
raise serializers.ValidationError(
"context_ct is required when context_object_id is provided"
)

if context_object_id and not context_ct:
raise serializers.ValidationError(
"context_object_id is required when context_ct is provided"
)

content_obj_model = apps.get_model(content_ct)
try:
content_obj_model.objects.get(id=content_object_id)
except content_obj_model.DoesNotExist as exc:
raise serializers.ValidationError("Invalid content_object_id") from exc

if context_ct:
context_object_model = apps.get_model(context_ct)
try:
context_object_model.objects.get(id=data.get("context_object_id"))
except context_object_model.DoesNotExist as exc:
raise serializers.ValidationError("Invalid context_object_id") from exc

return data

@property
def output_data(self) -> dict[str, Any]:
data = {
"content_ct": ct_from_label(self.validated_data["content_ct"]),
"content_object_id": self.validated_data["content_object_id"],
}
if not self.validated_data.get("context_ct"):
data.update(
{
"context_ct": ct_from_label(self.validated_data["context_ct"]),
"context_object_id": self.validated_data["context_object_id"],
}
)
return data
Empty file.
47 changes: 47 additions & 0 deletions recoco/apps/hitcount/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import json

import pytest
from django.contrib.auth.models import User
from django.urls import reverse
from model_bakery import baker
from rest_framework.test import APIClient

from recoco.apps.addressbook.models import Contact
from recoco.apps.hitcount.models import HitCount
from recoco.apps.hitcount.utils import ct_label
from recoco.apps.tasks.models import Task


@pytest.mark.django_db
def test_hit_view():
user = baker.make(User)
contact = baker.make(Contact)
recommendation = baker.make(Task)

api_client = APIClient()
api_client.force_authenticate(user=user)

url = reverse("api_hit")
headers = {"user-agent": "Mozilla/5.0"}
payload = json.dumps(
{
"content_ct": ct_label(contact),
"content_object_id": contact.id,
"context_ct": ct_label(recommendation),
"context_object_id": recommendation.id,
}
)

response = api_client.post(
path=url, headers=headers, data=payload, content_type="application/json"
)
assert response.status_code == 200

response = api_client.post(
path=url, headers=headers, data=payload, content_type="application/json"
)
assert response.status_code == 200

assert HitCount.objects.count() == 1
hitcount = HitCount.objects.first()
assert hitcount.hits.count() == 2
7 changes: 7 additions & 0 deletions recoco/apps/hitcount/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path

from .views import HitView

urlpatterns = [
path("hit/", HitView.as_view(), name="api_hit"),
]
12 changes: 12 additions & 0 deletions recoco/apps/hitcount/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model


def ct_label(obj: Model) -> str:
obj_content_type = ContentType.objects.get_for_model(obj)
return f"{obj_content_type.app_label}.{obj_content_type.model}"


def ct_from_label(content_type: str) -> ContentType:
app_label, model = content_type.split(".")
return ContentType.objects.get(app_label=app_label, model=model)
Loading

0 comments on commit 4b5f88f

Please sign in to comment.