Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions shop/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from shop.models import Presenter, Presentation, Participation, Coupon, Payment, PresentationTag

admin.site.register(Presenter)
admin.site.register(PresentationTag)


@admin.register(PresentationTag)
class PresentationTagAdmin(admin.ModelAdmin):
list_display = ('en_name', 'fa_name', 'color')
search_fields = ('en_name', 'fa_name')

@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
Expand All @@ -15,12 +20,14 @@ class PaymentAdmin(admin.ModelAdmin):
@admin.register(Participation)
class ParticipationAdmin(admin.ModelAdmin):
search_fields = ['user__phone_number']
list_display = ['__str__','payment_state', 'presentation__cost']
list_display = ['__str__', 'payment_state', 'is_capacity_exempt', 'presentation__cost']
list_filter = ['payment_state', 'is_capacity_exempt']


@admin.register(Coupon)
class CouponAdmin(admin.ModelAdmin):
list_display = ('name', 'used')
list_display = ('name', 'percentage', 'count', 'preserve_capacity', 'used')
filter_horizontal = ('eligible_presentations',)

def used(self, obj):
return Payment.objects.filter(payment_state="COMPLETED", coupon=obj).count()
Expand Down
36 changes: 36 additions & 0 deletions shop/migrations/0019_bilingual_presentation_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.db import migrations, models


def populate_persian_names(apps, schema_editor):
PresentationTag = apps.get_model("shop", "PresentationTag")
translations = {
"Online": "آنلاین",
"In-Person": "حضوری",
"Beginner": "مبتدی",
"Intermediate": "متوسط",
"Advanced": "پیشرفته",
}
for tag in PresentationTag.objects.all():
tag.fa_name = translations.get(tag.en_name, tag.en_name)
tag.save(update_fields=["fa_name"])


class Migration(migrations.Migration):
dependencies = [
("shop", "0018_launch_safety_constraints"),
]

operations = [
migrations.RenameField(
model_name="presentationtag",
old_name="name",
new_name="en_name",
),
migrations.AddField(
model_name="presentationtag",
name="fa_name",
field=models.CharField(default="", max_length=63),
preserve_default=False,
),
migrations.RunPython(populate_persian_names, migrations.RunPython.noop),
]
38 changes: 38 additions & 0 deletions shop/migrations/0020_coupon_eligible_presentations_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.1.5 on 2026-10-07 05:06

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('shop', '0019_bilingual_presentation_tags'),
]

operations = [
migrations.AddField(
model_name='coupon',
name='eligible_presentations',
field=models.ManyToManyField(blank=True, help_text='Leave empty to apply this coupon to the whole cart.', related_name='coupons', to='shop.presentation'),
),
migrations.AddField(
model_name='coupon',
name='preserve_capacity',
field=models.BooleanField(default=False, help_text="When enabled, registrations discounted by this coupon do not reduce the selected presentations' remaining capacity."),
),
migrations.AddField(
model_name='participation',
name='is_capacity_exempt',
field=models.BooleanField(default=False, help_text='Completed registrations with this flag do not reduce remaining capacity.'),
),
migrations.AddField(
model_name='payment',
name='coupon_participations',
field=models.ManyToManyField(blank=True, help_text="Snapshot of participations to which this payment's coupon applied.", related_name='coupon_payments', to='shop.participation'),
),
migrations.AddField(
model_name='payment',
name='coupon_preserves_capacity',
field=models.BooleanField(default=False, help_text="Snapshot of the coupon's capacity behavior when checkout started."),
),
]
33 changes: 31 additions & 2 deletions shop/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ def __str__(self):
return f'{self.last_name} {self.first_name}'

class PresentationTag(models.Model):
name = models.CharField(max_length=63)
en_name = models.CharField(max_length=63)
fa_name = models.CharField(max_length=63)
color = ColorField(default="#FA175C")

def __str__(self):
return self.name
return f"{self.fa_name} / {self.en_name}"


class Presentation(models.Model):
Expand Down Expand Up @@ -76,6 +77,7 @@ def get_remained_capacity(self):
# seat at the same time.
committed = Participation.objects.filter(
presentation=self,
is_capacity_exempt=False,
).filter(
models.Q(payment_state="COMPLETED")
| models.Q(
Expand All @@ -98,6 +100,10 @@ class Participation(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='participations')
presentation = models.ForeignKey(Presentation, on_delete=models.CASCADE, related_name='participations')
payment_state = models.CharField(choices=PAYMENT_STATES, default="PENDING", max_length=10)
is_capacity_exempt = models.BooleanField(
default=False,
help_text="Completed registrations with this flag do not reduce remaining capacity.",
)

class Meta:
constraints = [
Expand All @@ -115,6 +121,19 @@ class Coupon(models.Model):
name = models.CharField(max_length=50, primary_key=True, help_text="Don't use / in the name.")
count = models.PositiveIntegerField()
percentage = models.IntegerField(default=0.0, help_text='Enter a number between 0 to 100.')
eligible_presentations = models.ManyToManyField(
Presentation,
blank=True,
related_name="coupons",
help_text="Leave empty to apply this coupon to the whole cart.",
)
preserve_capacity = models.BooleanField(
default=False,
help_text=(
"When enabled, registrations discounted by this coupon do not reduce "
"the selected presentations' remaining capacity."
),
)

def __str__(self):
return self.name
Expand Down Expand Up @@ -143,6 +162,16 @@ class Payment(models.Model):
created_date = models.DateTimeField(auto_now_add=True)
verified_date = models.DateTimeField(null=True, blank=True)
coupon = models.ForeignKey(Coupon, on_delete=models.SET_NULL, default=None, null=True, blank=True)
coupon_participations = models.ManyToManyField(
Participation,
blank=True,
related_name="coupon_payments",
help_text="Snapshot of participations to which this payment's coupon applied.",
)
coupon_preserves_capacity = models.BooleanField(
default=False,
help_text="Snapshot of the coupon's capacity behavior when checkout started.",
)
accessories = models.ManyToManyField(Accessory, "payment_accessories")
is_competition_payment = models.BooleanField(default=False)

Expand Down
9 changes: 7 additions & 2 deletions shop/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ class Meta:
fields = ['first_name', 'last_name', 'email', 'description', 'avatar', 'linkedin']

class PresentationTagSerializer(serializers.ModelSerializer):
# Keep the legacy field until all deployed frontend clients use en_name.
name = serializers.CharField(source='en_name', read_only=True)

class Meta:
model = PresentationTag
fields = ['name', 'color']
fields = ['name', 'en_name', 'fa_name', 'color']

class PresentationSerializer(serializers.ModelSerializer):
remained_capacity = serializers.SerializerMethodField()
Expand Down Expand Up @@ -51,7 +54,9 @@ class Meta:
class CouponSerializer(serializers.ModelSerializer):
is_valid = serializers.SerializerMethodField()
class Meta:
fields = ['percentage', 'is_valid']
fields = [
'percentage', 'is_valid', 'eligible_presentations', 'preserve_capacity',
]
model = Coupon

def get_is_valid(self, obj):
Expand Down
Loading
Loading