Skip to content

Commit fcb8091

Browse files
committed
Likes, views and more...
1 parent 35052fc commit fcb8091

11 files changed

Lines changed: 306 additions & 26 deletions

File tree

core/admin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from django.contrib import admin
2-
from core.models import Like
2+
from core.models import Like, View
33

44

55
@admin.register(Like)
66
class LikeAdmin(admin.ModelAdmin):
77
list_display = ("id", "user", "content_type", "object_id", "content_object")
88
list_display_links = ("id", "user", "content_type", "object_id", "content_object")
9+
10+
11+
@admin.register(View)
12+
class ViewAdmin(admin.ModelAdmin):
13+
list_display = ("id", "user", "content_type", "object_id", "content_object")
14+
list_display_links = ("id", "user", "content_type", "object_id", "content_object")

core/migrations/0002_view.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Generated by Django 4.2 on 2023-06-11 10:28
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("contenttypes", "0002_remove_content_type_name"),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
("core", "0001_initial"),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name="View",
19+
fields=[
20+
(
21+
"id",
22+
models.BigAutoField(
23+
auto_created=True,
24+
primary_key=True,
25+
serialize=False,
26+
verbose_name="ID",
27+
),
28+
),
29+
("object_id", models.PositiveIntegerField()),
30+
(
31+
"content_type",
32+
models.ForeignKey(
33+
on_delete=django.db.models.deletion.CASCADE,
34+
related_name="views",
35+
to="contenttypes.contenttype",
36+
),
37+
),
38+
(
39+
"user",
40+
models.ForeignKey(
41+
on_delete=django.db.models.deletion.CASCADE,
42+
related_name="views",
43+
to=settings.AUTH_USER_MODEL,
44+
),
45+
),
46+
],
47+
options={
48+
"verbose_name": "Просмотр",
49+
"verbose_name_plural": "Просмотры",
50+
"unique_together": {("user", "content_type", "object_id")},
51+
},
52+
),
53+
]

core/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class Like(Model):
2424
)
2525
object_id = models.PositiveIntegerField()
2626
content_object = GenericForeignKey("content_type", "object_id")
27+
# is liked?
2728

2829
class Meta:
2930
unique_together = ("user", "content_type", "object_id")
@@ -32,3 +33,33 @@ class Meta:
3233

3334
def __str__(self):
3435
return f"Like<{self.user} - {self.content_object}>"
36+
37+
38+
class View(Model):
39+
"""
40+
Generic View model based on contenttype
41+
42+
Indicates if user has viewed the object
43+
44+
"""
45+
46+
user = models.ForeignKey(
47+
User,
48+
on_delete=models.CASCADE,
49+
related_name="views",
50+
)
51+
content_type = models.ForeignKey(
52+
ContentType,
53+
on_delete=models.CASCADE,
54+
related_name="views",
55+
)
56+
object_id = models.PositiveIntegerField()
57+
content_object = GenericForeignKey("content_type", "object_id")
58+
59+
class Meta:
60+
unique_together = ("user", "content_type", "object_id")
61+
verbose_name = "Просмотр"
62+
verbose_name_plural = "Просмотры"
63+
64+
def __str__(self):
65+
return f"View<{self.user} - {self.content_object}>"

core/serializers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from rest_framework import serializers
2+
3+
4+
class SetLikedSerializer(serializers.Serializer):
5+
is_liked = serializers.BooleanField()
6+
7+
8+
class SetViewedSerializer(serializers.Serializer):
9+
is_viewed = serializers.BooleanField()

core/services.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from django.contrib.auth import get_user_model
2+
from django.contrib.contenttypes.models import ContentType
3+
4+
from core.models import Like, View
5+
6+
User = get_user_model()
7+
8+
9+
def add_like(obj, user):
10+
obj_type = ContentType.objects.get_for_model(obj)
11+
like, is_created = Like.objects.get_or_create(
12+
content_type=obj_type, object_id=obj.id, user=user
13+
)
14+
return like
15+
16+
17+
def remove_like(obj, user):
18+
obj_type = ContentType.objects.get_for_model(obj)
19+
Like.objects.filter(content_type=obj_type, object_id=obj.id, user=user).delete()
20+
21+
22+
def is_fan(obj, user) -> bool:
23+
if not user.is_authenticated:
24+
return False
25+
obj_type = ContentType.objects.get_for_model(obj)
26+
likes = Like.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
27+
return likes.exists()
28+
29+
30+
def get_fans(obj):
31+
obj_type = ContentType.objects.get_for_model(obj)
32+
return User.objects.filter(likes__content_type=obj_type, likes__object_id=obj.id)
33+
34+
35+
def get_likes_count(obj):
36+
obj_type = ContentType.objects.get_for_model(obj)
37+
return User.objects.filter(
38+
likes__content_type=obj_type, likes__object_id=obj.id
39+
).count()
40+
41+
42+
def set_like(obj, user, is_liked):
43+
if is_liked:
44+
add_like(obj, user)
45+
else:
46+
remove_like(obj, user)
47+
48+
49+
def add_view(obj, user):
50+
obj_type = ContentType.objects.get_for_model(obj)
51+
view, is_created = View.objects.get_or_create(
52+
content_type=obj_type, object_id=obj.id, user=user
53+
)
54+
return view
55+
56+
57+
def remove_view(obj, user):
58+
obj_type = ContentType.objects.get_for_model(obj)
59+
View.objects.filter(content_type=obj_type, object_id=obj.id, user=user).delete()
60+
61+
62+
def is_viewer(obj, user) -> bool:
63+
if not user.is_authenticated:
64+
return False
65+
obj_type = ContentType.objects.get_for_model(obj)
66+
views = View.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
67+
return views.exists()
68+
69+
70+
def get_viewers(obj):
71+
obj_type = ContentType.objects.get_for_model(obj)
72+
return User.objects.filter(views__content_type=obj_type, views__object_id=obj.id)
73+
74+
75+
def get_views_count(obj):
76+
obj_type = ContentType.objects.get_for_model(obj)
77+
return User.objects.filter(
78+
views__content_type=obj_type, views__object_id=obj.id
79+
).count()
80+
81+
82+
def set_viewed(obj, user, is_viewed):
83+
if is_viewed:
84+
add_view(obj, user)
85+
else:
86+
remove_view(obj, user)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2 on 2023-06-11 10:28
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("projects", "0013_projectnews"),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name="projectnews",
15+
options={
16+
"verbose_name": "Новость проекта",
17+
"verbose_name_plural": "Новости проекта",
18+
},
19+
),
20+
migrations.RemoveField(
21+
model_name="projectnews",
22+
name="views_count",
23+
),
24+
]

projects/models.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.db import models
66
from django.db.models import UniqueConstraint
77

8-
from core.models import Like
8+
from core.models import Like, View
99
from industries.models import Industry
1010
from projects.constants import VERBOSE_STEPS
1111
from projects.managers import AchievementManager, ProjectManager
@@ -192,6 +192,7 @@ class Meta:
192192
]
193193

194194

195+
# fixme: move project news to another app?
195196
class ProjectNews(models.Model):
196197
"""
197198
Project news model
@@ -202,17 +203,29 @@ class ProjectNews(models.Model):
202203
on_delete=models.CASCADE,
203204
related_name="news",
204205
)
205-
text = models.TextField(null=False, blank=False)
206-
# todo
207-
views_count = models.PositiveIntegerField(default=0)
208-
likes = GenericRelation(Like, related_query_name="project_news")
206+
text = models.TextField(
207+
null=False,
208+
blank=False,
209+
)
210+
views = GenericRelation(
211+
View,
212+
related_query_name="project_views",
213+
)
214+
likes = GenericRelation(
215+
Like,
216+
related_query_name="project_news",
217+
)
209218
# todo: files
210219

211220
datetime_created = models.DateTimeField(
212-
verbose_name="Дата создания", null=False, auto_now_add=True
221+
verbose_name="Дата создания",
222+
null=False,
223+
auto_now_add=True,
213224
)
214225
datetime_updated = models.DateTimeField(
215-
verbose_name="Дата изменения", null=False, auto_now=True
226+
verbose_name="Дата изменения",
227+
null=False,
228+
auto_now=True,
216229
)
217230

218231
class Meta:

projects/permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def has_object_permission(self, request, view, obj):
6767
return False
6868

6969

70-
class IsNewsAuthorIsProjectLeader(BasePermission):
70+
class IsNewsAuthorIsProjectLeaderOrReadOnly(BasePermission):
7171
"""
7272
Allows access to update project news only to leader.
7373
"""

projects/serializers.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from django.contrib.auth import get_user_model
2-
from django.contrib.contenttypes.models import ContentType
32
from rest_framework import serializers
43

54
from core.fields import CustomListField
5+
from core.services import get_views_count, get_likes_count
66
from industries.models import Industry
77
from projects.models import Project, Achievement, Collaborator, ProjectNews
88
from projects.validators import validate_project
@@ -211,6 +211,7 @@ class Meta:
211211

212212

213213
class ProjectNewsListSerializer(serializers.ModelSerializer):
214+
views_count = serializers.SerializerMethodField()
214215
likes_count = serializers.SerializerMethodField()
215216
project_name = serializers.SerializerMethodField()
216217
project_image_address = serializers.SerializerMethodField()
@@ -221,11 +222,11 @@ def get_project_name(self, obj):
221222
def get_project_image_address(self, obj):
222223
return obj.project.image_address
223224

225+
def get_views_count(self, obj):
226+
return get_views_count(obj)
227+
224228
def get_likes_count(self, obj):
225-
obj_type = ContentType.objects.get_for_model(obj)
226-
return User.objects.filter(
227-
likes__content_type=obj_type, likes__object_id=obj.id
228-
).count()
229+
return get_likes_count(obj)
229230

230231
class Meta:
231232
model = ProjectNews
@@ -241,6 +242,7 @@ class Meta:
241242

242243

243244
class ProjectNewsDetailSerializer(serializers.ModelSerializer):
245+
views_count = serializers.SerializerMethodField()
244246
likes_count = serializers.SerializerMethodField()
245247
project_name = serializers.SerializerMethodField()
246248
project_image_address = serializers.SerializerMethodField()
@@ -251,11 +253,11 @@ def get_project_name(self, obj):
251253
def get_project_image_address(self, obj):
252254
return obj.project.image_address
253255

256+
def get_views_count(self, obj):
257+
return get_views_count(obj)
258+
254259
def get_likes_count(self, obj):
255-
obj_type = ContentType.objects.get_for_model(obj)
256-
return User.objects.filter(
257-
likes__content_type=obj_type, likes__object_id=obj.id
258-
).count()
260+
return get_likes_count(obj)
259261

260262
class Meta:
261263
model = ProjectNews

projects/urls.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,23 @@
1313
SetLikeOnProject,
1414
ProjectNewsList,
1515
ProjectNewsDetail,
16+
ProjectNewsDetailSetViewed,
17+
ProjectNewsDetailSetLiked,
1618
)
1719

1820
app_name = "projects"
1921

2022
urlpatterns = [
2123
path("", ProjectList.as_view()),
2224
path("<int:pk>/like/", SetLikeOnProject.as_view()),
23-
path("<int:pk>/news/", ProjectNewsList.as_view()),
25+
path("<int:project_pk>/news/", ProjectNewsList.as_view()),
2426
path("<int:project_pk>/news/<int:pk>/", ProjectNewsDetail.as_view()),
27+
path(
28+
"<int:project_pk>/news/<int:pk>/set_viewed/", ProjectNewsDetailSetViewed.as_view()
29+
),
30+
path(
31+
"<int:project_pk>/news/<int:pk>/set_liked/", ProjectNewsDetailSetLiked.as_view()
32+
),
2533
path("<int:pk>/collaborators/", ProjectCollaborators.as_view()),
2634
path("<int:pk>/", ProjectDetail.as_view()),
2735
path("<int:pk>/recommended_users", ProjectRecommendedUsers.as_view()),

0 commit comments

Comments
 (0)