Skip to content

Commit 0f1ac9d

Browse files
Merge pull request #61 from RandomProgramm3r/develop
refactor(feed): Simplify promo feed logic and related views This commit refactors the promo feed generation and comment handling to improve code structure, efficiency, and maintainability. Key changes: - Moved the complex feed generation logic from `UserFeedView` into a new `PromoManager.get_feed_for_user` method. This centralizes business logic in the manager layer, simplifying the view and making the query logic reusable. - The new manager method encapsulates filtering for active promos and targeting by user age and country. - Optimized category filtering to use a direct `__icontains` database query, which is more efficient than the previous Python-based filtering. - Introduced a `PromoObjectMixin` to reduce code duplication in comment-related views (`PromoCommentListCreateView`, `PromoCommentDetailView`) by abstracting the common task of retrieving the associated promo object.
2 parents a2bf351 + 54c0317 commit 0f1ac9d

File tree

2 files changed

+146
-151
lines changed

2 files changed

+146
-151
lines changed

promo_code/business/managers.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import django.contrib.auth.models
22
import django.db.models
3+
import django.utils.timezone
34

45
import business.constants
56
import business.models
@@ -52,6 +53,104 @@ def with_related(self):
5253
def for_company(self, user):
5354
return self.with_related().filter(company=user)
5455

56+
def get_feed_for_user(
57+
self,
58+
user,
59+
active_filter=None,
60+
user_country=None,
61+
user_age=None,
62+
):
63+
"""
64+
Retrieve a queryset of Promo objects for a given user, filtered
65+
and ordered according to specified criteria.
66+
"""
67+
today = django.utils.timezone.now().date()
68+
69+
qs = (
70+
self.get_queryset()
71+
.select_related('company')
72+
.annotate(_has_unique_codes=self._q_has_unique_codes())
73+
.filter(self._q_is_targeted(user_country, user_age))
74+
)
75+
76+
if active_filter is not None:
77+
is_active = active_filter.lower() == 'true'
78+
active_q = self._q_is_active(today)
79+
qs = qs.filter(active_q) if is_active else qs.exclude(active_q)
80+
81+
return qs.order_by('-created_at')
82+
83+
def _q_is_active(self, today):
84+
"""
85+
Build a Q expression that checks whether a promo
86+
is active on the given date.
87+
"""
88+
89+
qt = django.db.models.Q(active_from__lte=today) | django.db.models.Q(
90+
active_from__isnull=True,
91+
)
92+
tu = django.db.models.Q(active_until__gte=today) | django.db.models.Q(
93+
active_until__isnull=True,
94+
)
95+
96+
common = django.db.models.Q(
97+
mode=business.constants.PROMO_MODE_COMMON,
98+
used_count__lt=django.db.models.F('max_count'),
99+
)
100+
unique = django.db.models.Q(
101+
mode=business.constants.PROMO_MODE_UNIQUE,
102+
_has_unique_codes=True,
103+
)
104+
105+
return qt & tu & (common | unique)
106+
107+
def _q_has_unique_codes(self):
108+
"""
109+
Annotate whether there are unused unique codes remaining
110+
for each promo.
111+
"""
112+
subq = business.models.PromoCode.objects.filter(
113+
promo=django.db.models.OuterRef('pk'),
114+
is_used=False,
115+
)
116+
return django.db.models.Exists(subq)
117+
118+
def _q_is_targeted(self, country, age):
119+
"""
120+
Build a Q expression that checks whether a promo targets the given
121+
country and age, or is not targeted.
122+
"""
123+
empty = django.db.models.Q(target={})
124+
125+
if country:
126+
match_country = django.db.models.Q(target__country__iexact=country)
127+
else:
128+
match_country = django.db.models.Q()
129+
no_country = ~django.db.models.Q(
130+
target__has_key='country',
131+
) | django.db.models.Q(target__country__isnull=True)
132+
country_ok = match_country | no_country
133+
134+
no_age_limits = ~django.db.models.Q(
135+
target__has_key='age_from',
136+
) & ~django.db.models.Q(target__has_key='age_until')
137+
if age is None:
138+
age_ok = no_age_limits
139+
else:
140+
from_ok = (
141+
~django.db.models.Q(target__has_key='age_from')
142+
| django.db.models.Q(target__age_from__isnull=True)
143+
| django.db.models.Q(target__age_from__lte=age)
144+
)
145+
until_ok = (
146+
~django.db.models.Q(target__has_key='age_until')
147+
| django.db.models.Q(target__age_until__isnull=True)
148+
| django.db.models.Q(target__age_until__gte=age)
149+
)
150+
age_ok = no_age_limits | (from_ok & until_ok)
151+
152+
return empty | (country_ok & age_ok)
153+
55154
@django.db.transaction.atomic
56155
def create_promo(
57156
self,

promo_code/user/views.py

Lines changed: 47 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import django.db.models
22
import django.db.transaction
33
import django.shortcuts
4-
import django.utils.timezone
5-
import rest_framework.exceptions
64
import rest_framework.generics
75
import rest_framework.permissions
86
import rest_framework.response
@@ -11,10 +9,8 @@
119
import rest_framework_simplejwt.tokens
1210
import rest_framework_simplejwt.views
1311

14-
import business.constants
1512
import business.models
1613
import core.pagination
17-
import user.antifraud_service
1814
import user.models
1915
import user.permissions
2016
import user.serializers
@@ -76,25 +72,19 @@ class UserPromoDetailView(rest_framework.generics.RetrieveAPIView):
7672
Retrieve (GET) information about the promo without receiving a promo code.
7773
"""
7874

79-
queryset = (
80-
business.models.Promo.objects.select_related('company')
81-
.prefetch_related(
82-
'unique_codes',
83-
)
84-
.only(
85-
'id',
86-
'company__id',
87-
'company__name',
88-
'description',
89-
'image_url',
90-
'active',
91-
'active_from',
92-
'active_until',
93-
'mode',
94-
'used_count',
95-
'like_count',
96-
'comment_count',
97-
)
75+
queryset = business.models.Promo.objects.select_related('company').only(
76+
'id',
77+
'company__id',
78+
'company__name',
79+
'description',
80+
'image_url',
81+
'active',
82+
'active_from',
83+
'active_until',
84+
'mode',
85+
'used_count',
86+
'like_count',
87+
'comment_count',
9888
)
9989

10090
serializer_class = user.serializers.UserPromoDetailSerializer
@@ -113,116 +103,26 @@ class UserFeedView(rest_framework.generics.ListAPIView):
113103

114104
def get_queryset(self):
115105
user = self.request.user
116-
user_age = user.other.get('age')
117-
user_country_raw = user.other.get('country')
118-
user_country = user_country_raw.lower() if user_country_raw else None
119-
120-
queryset = business.models.Promo.objects.select_related('company')
121-
122-
today_utc = django.utils.timezone.now().date()
123-
124-
q_active_time = (
125-
django.db.models.Q(active_from__lte=today_utc)
126-
| django.db.models.Q(active_from__isnull=True)
127-
) & (
128-
django.db.models.Q(active_until__gte=today_utc)
129-
| django.db.models.Q(active_until__isnull=True)
130-
)
131-
132-
q_common_active = django.db.models.Q(
133-
mode=business.constants.PROMO_MODE_COMMON,
134-
used_count__lt=django.db.models.F('max_count'),
135-
)
136-
137-
has_available_unique_codes = business.models.PromoCode.objects.filter(
138-
promo=django.db.models.OuterRef('pk'),
139-
is_used=False,
140-
)
141-
142-
queryset = queryset.annotate(
143-
_has_available_unique_codes=django.db.models.Exists(
144-
has_available_unique_codes,
145-
),
146-
)
147-
q_unique_active = django.db.models.Q(
148-
mode=business.constants.PROMO_MODE_UNIQUE,
149-
_has_available_unique_codes=True,
150-
)
151-
152-
q_is_active_by_rules = q_active_time & (
153-
q_common_active | q_unique_active
154-
)
155-
156-
q_target_empty = django.db.models.Q(target={})
157-
158-
q_country_target_matches = django.db.models.Q()
159-
if user_country:
160-
q_country_target_matches = django.db.models.Q(
161-
target__country__iexact=user_country,
162-
)
163-
164-
q_country_target_not_set_or_empty = ~django.db.models.Q(
165-
target__has_key='country',
166-
) | django.db.models.Q(target__country__isnull=True)
167-
q_user_meets_country_target = (
168-
q_country_target_matches | q_country_target_not_set_or_empty
169-
)
170106

171-
q_age_target_not_set = ~django.db.models.Q(
172-
target__has_key='age_from',
173-
) & ~django.db.models.Q(target__has_key='age_until')
174-
q_user_meets_age_target = q_age_target_not_set
175-
176-
if user_age is not None:
177-
q_age_from_ok = (
178-
~django.db.models.Q(target__has_key='age_from')
179-
| django.db.models.Q(target__age_from__isnull=True)
180-
| django.db.models.Q(target__age_from__lte=user_age)
181-
)
182-
q_age_until_ok = (
183-
~django.db.models.Q(target__has_key='age_until')
184-
| django.db.models.Q(target__age_until__isnull=True)
185-
| django.db.models.Q(target__age_until__gte=user_age)
186-
)
187-
q_user_age_in_defined_range = q_age_from_ok & q_age_until_ok
188-
q_user_meets_age_target = (
189-
q_age_target_not_set | q_user_age_in_defined_range
190-
)
191-
192-
q_user_is_targeted = q_target_empty | (
193-
q_user_meets_country_target & q_user_meets_age_target
107+
user_age = user.other.get('age')
108+
user_country = user.other.get('country').lower()
109+
active_filter = self.request.query_params.get('active')
110+
111+
return business.models.Promo.objects.get_feed_for_user(
112+
user,
113+
active_filter=active_filter,
114+
user_country=user_country,
115+
user_age=user_age,
194116
)
195117

196-
queryset = queryset.filter(q_user_is_targeted)
197-
198-
active_param_str = self.request.query_params.get('active')
199-
if active_param_str is not None:
200-
active_param_bool = active_param_str.lower() == 'true'
201-
if active_param_bool:
202-
queryset = queryset.filter(q_is_active_by_rules)
203-
else:
204-
queryset = queryset.exclude(q_is_active_by_rules)
205-
206-
return queryset.order_by('-created_at')
207-
208118
def filter_queryset(self, queryset):
209119
queryset = super().filter_queryset(queryset)
210-
211120
category_param = self.request.query_params.get('category')
121+
212122
if category_param:
213-
category_param = category_param.lower()
214-
if category_param:
215-
filtered_pks = []
216-
for promo in queryset:
217-
target_categories = promo.target.get('categories')
218-
if not isinstance(target_categories, list):
219-
continue
220-
if any(
221-
cat_name.lower() == category_param
222-
for cat_name in target_categories
223-
):
224-
filtered_pks.append(promo.pk)
225-
queryset = queryset.filter(pk__in=filtered_pks)
123+
needle = f'"{category_param.lower()}"'
124+
queryset = queryset.filter(target__categories__icontains=needle)
125+
226126
return queryset
227127

228128
def list(self, request, *args, **kwargs):
@@ -288,7 +188,21 @@ def delete(self, request, id):
288188
)
289189

290190

291-
class PromoCommentListCreateView(rest_framework.generics.ListCreateAPIView):
191+
class PromoObjectMixin:
192+
"""Mixin for retrieving the Promo object and saving it to self.promo"""
193+
194+
def dispatch(self, request, *args, **kwargs):
195+
self.promo = django.shortcuts.get_object_or_404(
196+
business.models.Promo.objects.select_for_update(),
197+
pk=self.kwargs.get('promo_id'),
198+
)
199+
return super().dispatch(request, *args, **kwargs)
200+
201+
202+
class PromoCommentListCreateView(
203+
PromoObjectMixin,
204+
rest_framework.generics.ListCreateAPIView,
205+
):
292206
permission_classes = [rest_framework.permissions.IsAuthenticated]
293207

294208
pagination_class = core.pagination.CustomLimitOffsetPagination
@@ -299,28 +213,14 @@ def get_serializer_class(self):
299213
return user.serializers.CommentSerializer
300214

301215
def get_queryset(self):
302-
promo_id = self.kwargs.get('promo_id')
303-
try:
304-
promo = business.models.Promo.objects.get(pk=promo_id)
305-
except business.models.Promo.DoesNotExist:
306-
raise rest_framework.exceptions.NotFound(detail='Promo not found.')
307-
308216
return user.models.PromoComment.objects.filter(
309-
promo=promo,
217+
promo=self.promo,
310218
).select_related('author')
311219

312220
def perform_create(self, serializer):
313-
promo_id = self.kwargs.get('promo_id')
314-
try:
315-
promo = business.models.Promo.objects.get(pk=promo_id)
316-
except business.models.Promo.DoesNotExist:
317-
raise rest_framework.exceptions.ValidationError(
318-
{'promo_id': 'Promo not found.'},
319-
)
320-
321-
serializer.save(author=self.request.user, promo=promo)
322-
promo.comment_count = django.db.models.F('comment_count') + 1
323-
promo.save(update_fields=['comment_count'])
221+
serializer.save(author=self.request.user, promo=self.promo)
222+
self.promo.comment_count = django.db.models.F('comment_count') + 1
223+
self.promo.save(update_fields=['comment_count'])
324224

325225
def create(self, request, *args, **kwargs):
326226
create_serializer = self.get_serializer(data=request.data)
@@ -346,6 +246,7 @@ def list(self, request, *args, **kwargs):
346246

347247

348248
class PromoCommentDetailView(
249+
PromoObjectMixin,
349250
rest_framework.generics.RetrieveUpdateDestroyAPIView,
350251
):
351252
permission_classes = [
@@ -362,13 +263,8 @@ def get_serializer_class(self):
362263
return user.serializers.CommentSerializer
363264

364265
def get_queryset(self):
365-
promo_id = self.kwargs.get('promo_id')
366-
try:
367-
promo = business.models.Promo.objects.get(pk=promo_id)
368-
except business.models.Promo.DoesNotExist:
369-
raise rest_framework.exceptions.NotFound(detail='Promo not found.')
370266
return user.models.PromoComment.objects.filter(
371-
promo=promo,
267+
promo=self.promo,
372268
).select_related('author')
373269

374270
def update(self, request, *args, **kwargs):

0 commit comments

Comments
 (0)