-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
400 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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",) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.