diff --git a/lego/apps/achievements/constants.py b/lego/apps/achievements/constants.py index e911fd873..0aa9d7ae3 100644 --- a/lego/apps/achievements/constants.py +++ b/lego/apps/achievements/constants.py @@ -1,6 +1,7 @@ from typing import Callable, List, Tuple from lego.apps.users.models import User + from .verification import ( check_event_generic, check_event_price_over, @@ -12,6 +13,7 @@ EVENT_RANK_IDENTIFIER = "event_rank" EVENT_PRICE_IDENTIFIER = "event_price" QUOTE_IDENTIFIER = "quote_count" +MEETING_IDENTIFIER = "meeting_hidden" EVENT_ACHIEVEMENTS = { "arrangement_10": { @@ -154,7 +156,20 @@ }, } -HIDDEN_ACHIEVEMENTS = {**QUOTE_ACHIEVEMENTS} +MEETING_ACHIEVEMENTS = { + "meeting_hidden": { + "identifier": MEETING_IDENTIFIER, + "name": "Er det noen her?", + "description": "", + "image": "meeting.png", + "requirement_function": lambda user: False, + "hidden": True, + "rarity": 3, + "level": 0, + }, +} + +HIDDEN_ACHIEVEMENTS = {**QUOTE_ACHIEVEMENTS, **MEETING_ACHIEVEMENTS} ACHIEVEMENTS = { diff --git a/lego/apps/achievements/migrations/0001_initial.py b/lego/apps/achievements/migrations/0001_initial.py index b0beb8b85..a86b5d1d1 100644 --- a/lego/apps/achievements/migrations/0001_initial.py +++ b/lego/apps/achievements/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 4.0.10 on 2024-10-17 17:46 +# Generated by Django 4.0.10 on 2024-10-17 19:32 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -16,27 +16,180 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Achievement', + name="Achievement", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now, editable=False)), - ('updated_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), - ('deleted', models.BooleanField(db_index=True, default=False, editable=False)), - ('identifier', models.CharField(choices=[('event_count', 'event_count'), ('event_count', 'event_count'), ('event_count', 'event_count'), ('event_count', 'event_count'), ('event_count', 'event_count'), ('event_rank', 'event_rank'), ('event_rank', 'event_rank'), ('event_rank', 'event_rank'), ('quote_count', 'quote_count'), ('event_price', 'event_price'), ('event_price', 'event_price'), ('event_price', 'event_price')], max_length=128)), - ('name', models.CharField(choices=[('Arrangement: Nykommer', 'Arrangement: Nykommer'), ('Arrangement: Gjenganger', 'Arrangement: Gjenganger'), ('Arrangement: Stjerneskudd', 'Arrangement: Stjerneskudd'), ('Arrangement: Episenter', 'Arrangement: Episenter'), ('Arrangement: Legende', 'Arrangement: Legende'), ('Arrangement: Ikon', 'Arrangement: Ikon'), ('Arrangement: Mester', 'Arrangement: Mester'), ('Arrangement: Fyrtårn', 'Arrangement: Fyrtårn'), ('Psssst', 'Psssst'), ('Pappapenger', 'Pappapenger'), ('Arvingen', 'Arvingen'), ('Bærumsbaron', 'Bærumsbaron')], max_length=128)), - ('description', models.CharField(blank=True, choices=[('Deltatt på 10 arrangementer', 'Deltatt på 10 arrangementer'), ('Deltatt på 25 arrangementer', 'Deltatt på 25 arrangementer'), ('Deltatt på 50 arrangementer', 'Deltatt på 50 arrangementer'), ('Deltatt på 100 arrangementer', 'Deltatt på 100 arrangementer'), ('Deltatt på 200 arrangementer', 'Deltatt på 200 arrangementer'), ('#3 Flest arrangementer', '#3 Flest arrangementer'), ('#2 Flest arrangementer', '#2 Flest arrangementer'), ('#1 Flest arrangementer', '#1 Flest arrangementer'), ('', ''), ('Har betalt over 2500 i påmelding.', 'Har betalt over 2500 i påmelding.'), ('Har betalt over 5000 i påmelding.', 'Har betalt over 5000 i påmelding.'), ('Har betalt over 10000 i påmelding.', 'Har betalt over 10000 i påmelding.')], max_length=128)), - ('image', models.CharField(choices=[('bronze.png', 'bronze.png'), ('silver.png', 'silver.png'), ('gold.png', 'gold.png'), ('platinum.png', 'platinum.png'), ('platinum.png', 'platinum.png'), ('rank_3.png', 'rank_3.png'), ('rank_2.png', 'rank_2.png'), ('rank_1.png', 'rank_1.png'), ('psst.png', 'psst.png'), ('cash_1.png', 'cash_1.png'), ('cash_2.png', 'cash_2.png'), ('cash_3.png', 'cash_3.png')], max_length=128)), - ('hidden', models.BooleanField(default=False)), - ('relative_index', models.PositiveSmallIntegerField(default=0)), - ('rarity', models.PositiveSmallIntegerField(default=0)), - ('level', models.PositiveSmallIntegerField(default=0)), - ('created_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='achievements', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "updated_at", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "deleted", + models.BooleanField(db_index=True, default=False, editable=False), + ), + ( + "identifier", + models.CharField( + choices=[ + ("event_count", "event_count"), + ("event_count", "event_count"), + ("event_count", "event_count"), + ("event_count", "event_count"), + ("event_count", "event_count"), + ("event_rank", "event_rank"), + ("event_rank", "event_rank"), + ("event_rank", "event_rank"), + ("quote_count", "quote_count"), + ("meeting_hidden", "meeting_hidden"), + ("event_price", "event_price"), + ("event_price", "event_price"), + ("event_price", "event_price"), + ], + max_length=128, + ), + ), + ( + "name", + models.CharField( + choices=[ + ("Arrangement: Nykommer", "Arrangement: Nykommer"), + ("Arrangement: Gjenganger", "Arrangement: Gjenganger"), + ("Arrangement: Stjerneskudd", "Arrangement: Stjerneskudd"), + ("Arrangement: Episenter", "Arrangement: Episenter"), + ("Arrangement: Legende", "Arrangement: Legende"), + ("Arrangement: Ikon", "Arrangement: Ikon"), + ("Arrangement: Mester", "Arrangement: Mester"), + ("Arrangement: Fyrtårn", "Arrangement: Fyrtårn"), + ("Psssst", "Psssst"), + ("Er det noen her?", "Er det noen her?"), + ("Pappapenger", "Pappapenger"), + ("Arvingen", "Arvingen"), + ("Bærumsbaron", "Bærumsbaron"), + ], + max_length=128, + ), + ), + ( + "description", + models.CharField( + blank=True, + choices=[ + ( + "Deltatt på 10 arrangementer", + "Deltatt på 10 arrangementer", + ), + ( + "Deltatt på 25 arrangementer", + "Deltatt på 25 arrangementer", + ), + ( + "Deltatt på 50 arrangementer", + "Deltatt på 50 arrangementer", + ), + ( + "Deltatt på 100 arrangementer", + "Deltatt på 100 arrangementer", + ), + ( + "Deltatt på 200 arrangementer", + "Deltatt på 200 arrangementer", + ), + ("#3 Flest arrangementer", "#3 Flest arrangementer"), + ("#2 Flest arrangementer", "#2 Flest arrangementer"), + ("#1 Flest arrangementer", "#1 Flest arrangementer"), + ("", ""), + ("", ""), + ( + "Har betalt over 2500 i påmelding.", + "Har betalt over 2500 i påmelding.", + ), + ( + "Har betalt over 5000 i påmelding.", + "Har betalt over 5000 i påmelding.", + ), + ( + "Har betalt over 10.000 i påmelding.", + "Har betalt over 10.000 i påmelding.", + ), + ], + max_length=128, + ), + ), + ( + "image", + models.CharField( + choices=[ + ("bronze.png", "bronze.png"), + ("silver.png", "silver.png"), + ("gold.png", "gold.png"), + ("platinum.png", "platinum.png"), + ("platinum.png", "platinum.png"), + ("rank_3.png", "rank_3.png"), + ("rank_2.png", "rank_2.png"), + ("rank_1.png", "rank_1.png"), + ("psst.png", "psst.png"), + ("meeting.png", "meeting.png"), + ("cash_1.png", "cash_1.png"), + ("cash_2.png", "cash_2.png"), + ("cash_3.png", "cash_3.png"), + ], + max_length=128, + ), + ), + ("hidden", models.BooleanField(default=False)), + ("relative_index", models.PositiveSmallIntegerField(default=0)), + ("rarity", models.PositiveSmallIntegerField(default=0)), + ("level", models.PositiveSmallIntegerField(default=0)), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="achievements", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, - 'default_manager_name': 'objects', + "abstract": False, + "default_manager_name": "objects", }, ), ] diff --git a/lego/apps/achievements/models.py b/lego/apps/achievements/models.py index a8bf46bbe..82b95adc8 100644 --- a/lego/apps/achievements/models.py +++ b/lego/apps/achievements/models.py @@ -3,22 +3,33 @@ from lego.apps.users.models import User from lego.utils.models import BasisModel -from .constants import ACHIEVEMENT_IDENTIFIERS, ACHIEVEMENT_NAMES, ACHIEVEMENT_DESCRIPTIONS, ACHIEVEMENT_IMAGES +from .constants import ( + ACHIEVEMENT_DESCRIPTIONS, + ACHIEVEMENT_IDENTIFIERS, + ACHIEVEMENT_IMAGES, + ACHIEVEMENT_NAMES, +) + + class Achievement(BasisModel): - identifier=models.CharField(choices=ACHIEVEMENT_IDENTIFIERS, max_length=128) - name=models.CharField(choices=ACHIEVEMENT_NAMES, max_length=128) - description=models.CharField(choices=ACHIEVEMENT_DESCRIPTIONS, blank=True, max_length=128) - image=models.CharField(choices=ACHIEVEMENT_IMAGES, null=False, max_length=128) - hidden=models.BooleanField(default=False,null=False,blank=False) + identifier = models.CharField(choices=ACHIEVEMENT_IDENTIFIERS, max_length=128) + name = models.CharField(choices=ACHIEVEMENT_NAMES, max_length=128) + description = models.CharField( + choices=ACHIEVEMENT_DESCRIPTIONS, blank=True, max_length=128 + ) + image = models.CharField(choices=ACHIEVEMENT_IMAGES, null=False, max_length=128) + hidden = models.BooleanField(default=False, null=False, blank=False) user = models.ForeignKey( User, related_name="achievements", on_delete=models.CASCADE ) - relative_index=models.PositiveSmallIntegerField(default=0) - rarity=models.PositiveSmallIntegerField(default=0) - level=models.PositiveSmallIntegerField(default=0) + relative_index = models.PositiveSmallIntegerField(default=0) + rarity = models.PositiveSmallIntegerField(default=0) + level = models.PositiveSmallIntegerField(default=0) @property def percentage(self): total_users = User.objects.count() or 1 - achievement_users = Achievement.objects.filter(name=self.name).values('user').distinct().count() + achievement_users = ( + Achievement.objects.filter(name=self.name).values("user").distinct().count() + ) return (achievement_users / total_users) * 100 diff --git a/lego/apps/achievements/promotion.py b/lego/apps/achievements/promotion.py index 268aae672..6c8a71d83 100644 --- a/lego/apps/achievements/promotion.py +++ b/lego/apps/achievements/promotion.py @@ -1,32 +1,43 @@ -from lego.apps.achievements.models import Achievement -from django.utils import timezone from django.db.models import Count -from lego.apps.users.models import User -from lego.apps.events.models import Registration +from django.utils import timezone + +from lego.apps.achievements.models import Achievement from lego.apps.events.constants import SUCCESS_REGISTER +from lego.apps.events.models import Registration +from lego.apps.meetings.models import Meeting +from lego.apps.users.models import User + from .constants import ( EVENT_ACHIEVEMENTS, EVENT_IDENTIFIER, EVENT_PRICE_ACHIEVEMENTS, EVENT_PRICE_IDENTIFIER, EVENT_RANK_ACHIEVEMENTS, + MEETING_ACHIEVEMENTS, QUOTE_ACHIEVEMENTS, QUOTE_IDENTIFIER, ) def check_leveled_promotions(user: User, identifier: str, input_achievements: dict): - current_achievement = Achievement.objects.filter(user=user, identifier=identifier).first() - + current_achievement = Achievement.objects.filter( + user=user, identifier=identifier + ).first() + if not current_achievement: level = 0 initial_achievement_key = next( - (key for key, data in input_achievements.items() - if data.get("identifier") == identifier and data.get("level") == level), - None + ( + key + for key, data in input_achievements.items() + if data.get("identifier") == identifier and data.get("level") == level + ), + None, ) - - if initial_achievement_key and input_achievements[initial_achievement_key]["requirement_function"](user): + + if initial_achievement_key and input_achievements[initial_achievement_key][ + "requirement_function" + ](user): initial_data = input_achievements[initial_achievement_key] current_achievement = Achievement.objects.create( user=user, @@ -36,22 +47,26 @@ def check_leveled_promotions(user: User, identifier: str, input_achievements: di image=initial_data["image"], hidden=initial_data["hidden"], level=level, - rarity=initial_data["rarity"] + rarity=initial_data["rarity"], ) else: - return + return next_level = current_achievement.level + 1 - + while True: next_achievement_key = next( - (key for key, data in input_achievements.items() - if data.get("identifier") == identifier and data.get("level") == next_level), - None + ( + key + for key, data in input_achievements.items() + if data.get("identifier") == identifier + and data.get("level") == next_level + ), + None, ) if not next_achievement_key: - break + break next_achievement_data = input_achievements[next_achievement_key] @@ -63,9 +78,9 @@ def check_leveled_promotions(user: User, identifier: str, input_achievements: di current_achievement.level = next_level current_achievement.rarity = next_achievement_data["rarity"] current_achievement.save() - next_level += 1 + next_level += 1 else: - break + break def check_rank_promotions(): @@ -106,13 +121,35 @@ def get_top_rank_users() -> dict: ).delete() +def check_meeting_hidden(owner: User, user: User, meeting: Meeting): + if owner == user and meeting.invited_users.count() == 1: + if not Achievement.objects.filter( + user=user, identifier=MEETING_ACHIEVEMENTS["meeting_hidden"]["identifier"] + ).exists(): + + Achievement.objects.create( + identifier=MEETING_ACHIEVEMENTS["meeting_hidden"]["identifier"], + name=MEETING_ACHIEVEMENTS["meeting_hidden"]["name"], + description=MEETING_ACHIEVEMENTS["meeting_hidden"]["description"], + image=MEETING_ACHIEVEMENTS["meeting_hidden"]["image"], + hidden=MEETING_ACHIEVEMENTS["meeting_hidden"]["hidden"], + user=user, + rarity=MEETING_ACHIEVEMENTS["meeting_hidden"]["rarity"], + level=MEETING_ACHIEVEMENTS["meeting_hidden"]["level"], + ) + return True + return False + + def check_event_related_single_user(user: User): check_leveled_promotions(user, EVENT_IDENTIFIER, EVENT_ACHIEVEMENTS) check_leveled_promotions(user, EVENT_PRICE_IDENTIFIER, EVENT_PRICE_ACHIEVEMENTS) + def check_quote_related_single_user(user: User): check_leveled_promotions(user, QUOTE_IDENTIFIER, QUOTE_ACHIEVEMENTS) + def check_all_promotions(): for user in User.objects.all(): check_quote_related_single_user(user) diff --git a/lego/apps/achievements/serializers.py b/lego/apps/achievements/serializers.py index c891341d2..956156f1b 100644 --- a/lego/apps/achievements/serializers.py +++ b/lego/apps/achievements/serializers.py @@ -1,7 +1,9 @@ +from django.db import transaction +from rest_framework import serializers + from lego.apps.achievements.models import Achievement from lego.utils.serializers import BasisModelSerializer -from rest_framework import serializers -from django.db import transaction + class AchievementSerializer(BasisModelSerializer): @@ -17,4 +19,3 @@ class Meta: "relative_index", "rarity", ) - diff --git a/lego/apps/achievements/tasks.py b/lego/apps/achievements/tasks.py index b73088a41..6087d179f 100644 --- a/lego/apps/achievements/tasks.py +++ b/lego/apps/achievements/tasks.py @@ -1,12 +1,9 @@ - +from lego import celery_app from lego.apps.achievements.promotion import check_all_promotions from lego.apps.events.tasks import AsyncRegister -from lego import celery_app @celery_app.task(base=AsyncRegister, bind=True) def run_all_promotions(self, logger_context=None): self.setup_logger(logger_context) check_all_promotions() - - diff --git a/lego/apps/achievements/verification.py b/lego/apps/achievements/verification.py index 203d7667f..460d09aa5 100644 --- a/lego/apps/achievements/verification.py +++ b/lego/apps/achievements/verification.py @@ -1,25 +1,30 @@ -from django.utils import timezone from django.db.models import Count, Sum +from django.utils import timezone from lego.apps.events import constants +from lego.apps.events.constants import PAYMENT_MANUAL, PAYMENT_SUCCESS, SUCCESS_REGISTER +from lego.apps.events.models import Registration from lego.apps.quotes.models import Quote from lego.apps.users.models import User -from lego.apps.events.models import Registration -from lego.apps.events.constants import PAYMENT_MANUAL, PAYMENT_SUCCESS, SUCCESS_REGISTER + def check_event_generic(user: User, count: int): - return len( - Registration.objects.filter( - user=user, - status=SUCCESS_REGISTER, - event__end_time__lte=timezone.now() + return ( + len( + Registration.objects.filter( + user=user, status=SUCCESS_REGISTER, event__end_time__lte=timezone.now() + ) ) - ) >= count + >= count + ) -#This function should not be used (ideally we let the cron job handle this with a different function) + +# This function should not be used (ideally we let the cron job handle this with a different function) def check_event_rank(user: User, rank: int): top_users = ( - Registration.objects.filter(status=SUCCESS_REGISTER, event__end_time__lte=timezone.now()) + Registration.objects.filter( + status=SUCCESS_REGISTER, event__end_time__lte=timezone.now() + ) .values("user") .annotate(event_count=Count("id")) .order_by("-event_count")[:3] @@ -28,17 +33,21 @@ def check_event_rank(user: User, rank: int): rank_mapping = {entry["user"]: idx + 1 for idx, entry in enumerate(top_users)} return rank_mapping.get(user.id) == rank + def check_verified_quote(user: User): return Quote.objects.filter(approved=True, created_by=user).exists() + +# There is a case where manual payment does not update the payment amount. I have not changed this code so only stripe payments will count. def check_event_price_over(user: User, price: int): total_paid = ( Registration.objects.filter( user=user, event__is_priced=True, status=SUCCESS_REGISTER, - payment_status__in=[PAYMENT_SUCCESS, PAYMENT_MANUAL] - ) - .aggregate(total=Sum("payment_amount"))["total"] or 0 + payment_status__in=[PAYMENT_SUCCESS, PAYMENT_MANUAL], + event__end_time__lte=timezone.now(), + ).aggregate(total=Sum("payment_amount"))["total"] + or 0 ) - return total_paid > price \ No newline at end of file + return total_paid > price diff --git a/lego/apps/achievements/views.py b/lego/apps/achievements/views.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lego/apps/events/views.py b/lego/apps/events/views.py index bd259fe31..500f5b96d 100644 --- a/lego/apps/events/views.py +++ b/lego/apps/events/views.py @@ -16,7 +16,10 @@ import requests from celery.canvas import chain -from lego.apps.achievements.promotion import check_all_promotions, check_event_related_single_user +from lego.apps.achievements.promotion import ( + check_all_promotions, + check_event_related_single_user, +) from lego.apps.events import constants from lego.apps.events.exceptions import ( APIEventNotFound, @@ -438,7 +441,7 @@ def create(self, request, *args, **kwargs): registration.save(current_user=current_user) transaction.on_commit(lambda: async_register.delay(registration.id)) check_event_related_single_user(current_user) - #This is for testing!!!!: + # This is for testing!!!!: check_all_promotions() registration.refresh_from_db() registration_serializer = RegistrationReadSerializer( diff --git a/lego/apps/meetings/views.py b/lego/apps/meetings/views.py index 497edfa40..7e57b7643 100644 --- a/lego/apps/meetings/views.py +++ b/lego/apps/meetings/views.py @@ -2,6 +2,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.response import Response +from lego.apps.achievements.promotion import check_meeting_hidden from lego.apps.meetings.authentication import MeetingInvitationTokenAuthentication from lego.apps.meetings.filters import MeetingFilterSet from lego.apps.meetings.models import Meeting, MeetingInvitation @@ -47,7 +48,7 @@ def get_serializer_class(self): detail=True, methods=["POST"], serializer_class=MeetingUserInvite ) def invite_user(self, request, *args, **kwargs): - meeting = self.get_object() + meeting: Meeting = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data["user"] @@ -65,7 +66,10 @@ def bulk_invite(self, request, *args, **kwargs): groups = serializer.validated_data["groups"] if not len(users) and not len(groups): raise ValidationError({"error": "No users or groups given"}) - + if len(users) == 1: + check_meeting_hidden( + owner=meeting.created_by, user=users[0], meeting=meeting + ) for user in users: meeting.invite_user(user, request.user) for group in groups: diff --git a/lego/apps/users/serializers/users.py b/lego/apps/users/serializers/users.py index c840fa631..30ce917f9 100644 --- a/lego/apps/users/serializers/users.py +++ b/lego/apps/users/serializers/users.py @@ -25,7 +25,6 @@ class PublicUserSerializer(serializers.ModelSerializer): ) achievements = AchievementSerializer(many=True) - class Meta: model = User fields = (