From d82b3c29f245a125d06eff084dbfe19d0980dbf3 Mon Sep 17 00:00:00 2001 From: lsabor Date: Wed, 8 Jan 2025 12:31:32 -0800 Subject: [PATCH 1/7] add finalize flag help text on leaderboard time fields filtering adjustments to update leaderbaord finalize operation --- projects/models.py | 31 ++++ ...zed_alter_leaderboard_end_time_and_more.py | 48 ++++++ scoring/models.py | 150 ++++++++++-------- scoring/utils.py | 19 ++- 4 files changed, 184 insertions(+), 64 deletions(-) create mode 100644 scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py diff --git a/projects/models.py b/projects/models.py index 266714d9c..35932752c 100644 --- a/projects/models.py +++ b/projects/models.py @@ -1,3 +1,5 @@ +import logging + from django.db import models from django.db.models import ( Count, @@ -17,6 +19,8 @@ from users.models import User from utils.models import validate_alpha_slug, TimeStampedModel, TranslatedModel +logger = logging.getLogger(__name__) + class ProjectsQuerySet(models.QuerySet): def filter_topic(self): @@ -288,10 +292,34 @@ def __str__(self): def save(self, *args, **kwargs): creating = not self.pk + # Check if the primary leaderboard is associated with this project if self.primary_leaderboard and self.primary_leaderboard.project != self: raise ValueError( "Primary leaderboard must be associated with this project." ) + # If updating this project's end_date (and this isn't site_main), + # update the finalize_time of all leaderboards + if not creating: + previous = Project.objects.get(pk=self.pk) + if ( + self.close_date + and self.close_date != previous.close_date + and self.type + in ( + self.ProjectTypes.TOURNAMENT, + self.ProjectTypes.QUESTION_SERIES, + ) + ): + from scoring.models import Leaderboard + + # warn since this might not be an expected side effect + logger.warning( + f"Updating finalize_time for leaderboards of project {self.pk}" + ) + Leaderboard.objects.filter(project=self).update( + finalize_time=self.close_date + ) + super().save(*args, **kwargs) if ( creating @@ -300,6 +328,7 @@ def save(self, *args, **kwargs): in ( self.ProjectTypes.TOURNAMENT, self.ProjectTypes.QUESTION_SERIES, + self.ProjectTypes.COMMUNITY, ) ): # create default leaderboard when creating a new tournament/question series @@ -308,6 +337,8 @@ def save(self, *args, **kwargs): leaderboard = Leaderboard.objects.create( project=self, score_type=Leaderboard.ScoreTypes.PEER_TOURNAMENT, + start_time=self.start_date, + finalize_time=self.close_date, ) Project.objects.filter(pk=self.pk).update(primary_leaderboard=leaderboard) diff --git a/scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py b/scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py new file mode 100644 index 000000000..d69a43fad --- /dev/null +++ b/scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.4 on 2025-01-08 20:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("scoring", "0005_alter_score_aggregation_method_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="leaderboard", + name="finalized", + field=models.BooleanField( + default=False, + help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time.", + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="end_time", + field=models.DateTimeField( + blank=True, + help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field.\n ", + null=True, + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="finalize_time", + field=models.DateTimeField( + blank=True, + help_text="This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for:\n

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date.\n ", + null=True, + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="start_time", + field=models.DateTimeField( + blank=True, + help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords.\n ", + null=True, + ), + ), + ] diff --git a/scoring/models.py b/scoring/models.py index f97078527..2127e56bd 100644 --- a/scoring/models.py +++ b/scoring/models.py @@ -161,9 +161,37 @@ def get_base_score(cls, score_type: str) -> Score.ScoreTypes: ) score_type = models.CharField(max_length=200, choices=ScoreTypes.choices) - start_time = models.DateTimeField(null=True, blank=True) - end_time = models.DateTimeField(null=True, blank=True) - finalize_time = models.DateTimeField(null=True, blank=True) + start_time = models.DateTimeField( + null=True, + blank=True, + help_text="""This field is optional. It is used for: +

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered. +

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change. +

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. + """, + ) + end_time = models.DateTimeField( + null=True, + blank=True, + help_text="""This field is optional. It is used for: +

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead. +

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change. +

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field. + """, + ) + finalize_time = models.DateTimeField( + null=True, + blank=True, + help_text="""This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for: +

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. +

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized). +

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date. + """, + ) + finalized = models.BooleanField( + default=False, + help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time.", + ) def __str__(self): if self.name: @@ -177,72 +205,70 @@ def get_questions(self) -> QuerySet[Question]: related_posts__post__curation_status=Post.CurationStatus.APPROVED ) - if self.project and self.project.type == Project.ProjectTypes.SITE_MAIN: - # global leaderboard - if self.start_time is None or self.end_time is None: - raise ValueError("Global leaderboards must have start and end times") - - questions = questions.filter_public().filter( - related_posts__post__in=Post.objects.filter_for_main_feed() - ) - - if self.score_type == self.ScoreTypes.COMMENT_INSIGHT: - # post must be published - return questions.filter( - related_posts__post__published_at__lt=self.end_time - ) - elif self.score_type == self.ScoreTypes.QUESTION_WRITING: - # post must be published, and can't be resolved before the start_time - # of the leaderboard - return questions.filter( - Q(scheduled_close_time__gte=self.start_time) - & ( - Q(actual_close_time__isnull=True) - | Q(actual_close_time__gte=self.start_time) - ), - related_posts__post__published_at__lt=self.end_time, + if not (self.project and self.project.type == Project.ProjectTypes.SITE_MAIN): + # normal Project leaderboard + if self.project: + questions = questions.filter( + Q(related_posts__post__projects=self.project) + | Q(related_posts__post__default_project=self.project), ) + return questions - close_grace_period = timedelta(days=3) - resolve_grace_period = timedelta(days=100) + # global leaderboard + if self.start_time is None or self.end_time is None: + raise ValueError("Global leaderboards must have start and end times") - questions = questions.filter( - Q(actual_resolve_time__isnull=True) - | Q(actual_resolve_time__lte=self.end_time + resolve_grace_period), - open_time__gte=self.start_time, - open_time__lt=self.end_time, - scheduled_close_time__lte=self.end_time + close_grace_period, + questions = questions.filter_public().filter( + related_posts__post__in=Post.objects.filter_for_main_feed() + ) + + if self.score_type == self.ScoreTypes.COMMENT_INSIGHT: + # post must be published + return questions.filter(related_posts__post__published_at__lt=self.end_time) + elif self.score_type == self.ScoreTypes.QUESTION_WRITING: + # post must be published, and can't be resolved before the start_time + # of the leaderboard + return questions.filter( + Q(scheduled_close_time__gte=self.start_time) + & ( + Q(actual_close_time__isnull=True) + | Q(actual_close_time__gte=self.start_time) + ), + related_posts__post__published_at__lt=self.end_time, ) - gl_dates = global_leaderboard_dates() - checked_intervals: list[tuple[datetime, datetime]] = [] - for start, end in gl_dates[::-1]: # must be in reverse order, biggest first - if ( - (self.start_time, self.end_time) == (start, end) - or start < self.start_time - or self.end_time < end - ): - continue - to_add = True - for checked_start, checked_end in checked_intervals: - if checked_start < start and end < checked_end: - to_add = False - break - if to_add: - checked_intervals.append((start, end)) - questions = questions.filter( - Q(open_time__lt=start) - | Q(scheduled_close_time__gt=end + close_grace_period) - | Q(actual_resolve_time__gt=end + resolve_grace_period) - ) + close_grace_period = timedelta(days=3) + resolve_grace_period = timedelta(days=100) - return questions + questions = questions.filter( + Q(actual_resolve_time__isnull=True) + | Q(actual_resolve_time__lte=self.end_time + resolve_grace_period), + open_time__gte=self.start_time, + open_time__lt=self.end_time, + scheduled_close_time__lte=self.end_time + close_grace_period, + ) - if self.project: - return questions.filter( - Q(related_posts__post__projects=self.project) - | Q(related_posts__post__default_project=self.project) - ) + gl_dates = global_leaderboard_dates() + checked_intervals: list[tuple[datetime, datetime]] = [] + for start, end in gl_dates[::-1]: # must be in reverse order, biggest first + if ( + (self.start_time, self.end_time) == (start, end) + or start < self.start_time + or self.end_time < end + ): + continue + to_add = True + for checked_start, checked_end in checked_intervals: + if checked_start < start and end < checked_end: + to_add = False + break + if to_add: + checked_intervals.append((start, end)) + questions = questions.filter( + Q(open_time__lt=start) + | Q(scheduled_close_time__gt=end + close_grace_period) + | Q(actual_resolve_time__gt=end + resolve_grace_period) + ) return questions diff --git a/scoring/utils.py b/scoring/utils.py index 4570ff10c..9d1fe14ff 100644 --- a/scoring/utils.py +++ b/scoring/utils.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from decimal import Decimal from io import StringIO +import logging import numpy as np from django.db import transaction @@ -29,6 +30,8 @@ from utils.the_math.formulas import string_location_to_bucket_index from utils.the_math.measures import decimal_h_index +logger = logging.getLogger(__name__) + def score_question( question: Question, @@ -84,6 +87,7 @@ def generate_scoring_leaderboard_entries( if leaderboard.finalize_time: qs_filters["question__scheduled_close_time__lte"] = leaderboard.finalize_time + qs_filters["question__resolution_set_time__lte"] = leaderboard.finalize_time archived_scores = ArchivedScore.objects.filter(**qs_filters).prefetch_related( "question" @@ -398,6 +402,7 @@ def assign_prizes( def update_project_leaderboard( project: Project | None = None, leaderboard: Leaderboard | None = None, + force_update: bool = False, ) -> list[LeaderboardEntry]: if project is None and leaderboard is None: raise ValueError("Either project or leaderboard must be provided") @@ -410,6 +415,12 @@ def update_project_leaderboard( if leaderboard.score_type == Leaderboard.ScoreTypes.MANUAL: return list(leaderboard.entries.all().order_by("rank")) + if not force_update and leaderboard.finalized: + logger.warning( + "Leaderboard %s is already finalized, not updating", leaderboard.name + ) + return list(leaderboard.entries.all().order_by("rank")) + # new entries new_entries = generate_project_leaderboard(project, leaderboard) @@ -440,6 +451,8 @@ def update_project_leaderboard( # add prize if applicable if project.prize_pool: new_entries = assign_prizes(new_entries, project.prize_pool) + # set finalize + Leaderboard.objects.filter(pk=leaderboard.pk).update(finalized=True) # save entries previous_entries_map = { @@ -644,10 +657,12 @@ def get_contributions( if leaderboard.finalize_time: calculated_scores = calculated_scores.filter( - question__scheduled_close_time__lte=leaderboard.finalize_time + question__scheduled_close_time__lte=leaderboard.finalize_time, + question__resolution_set_time__lte=leaderboard.finalize_time, ).prefetch_related("question") archived_scores = archived_scores.filter( - question__scheduled_close_time__lte=leaderboard.finalize_time + question__scheduled_close_time__lte=leaderboard.finalize_time, + question__resolution_set_time__lte=leaderboard.finalize_time, ).prefetch_related("question") scores = list(archived_scores) From b887b895efd54eac632e58eb5424d31c2933ddbd Mon Sep 17 00:00:00 2001 From: lsabor Date: Wed, 8 Jan 2025 12:38:29 -0800 Subject: [PATCH 2/7] add force update leaderboard to admin panel --- scoring/admin.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scoring/admin.py b/scoring/admin.py index dae4ae1a6..61b3e6a5c 100644 --- a/scoring/admin.py +++ b/scoring/admin.py @@ -77,7 +77,11 @@ class LeaderboardAdmin(admin.ModelAdmin): AutocompleteFilterFactory("Project", "project"), ] inlines = [LeaderboardEntryInline] - actions = ["make_primary_leaderboard", "update_leaderboards"] + actions = [ + "make_primary_leaderboard", + "update_leaderboards", + "force_update_leaderboards", + ] def make_primary_leaderboard(self, request, queryset): for leaderboard in queryset: @@ -102,6 +106,16 @@ def update_leaderboards(self, request, queryset): update_leaderboards.short_description = "Update selected Leaderboards" + def force_update_leaderboards(self, request, queryset): + leaderboard: Leaderboard + for leaderboard in queryset: + update_project_leaderboard(leaderboard.project, leaderboard, force=True) + + force_update_leaderboards.short_description = ( + "Force update selected Leaderboards. " + "Will update even if leaderboard is finalized" + ) + @admin.register(LeaderboardEntry) class LeaderboardEntryAdmin(admin.ModelAdmin): From ba8dfedc87c838e8fc7ed85b276c2ee47d3c6902 Mon Sep 17 00:00:00 2001 From: lsabor Date: Wed, 15 Jan 2025 12:17:58 -0800 Subject: [PATCH 3/7] unconflict migrations --- ...zed_alter_leaderboard_end_time_and_more.py | 48 ------------------- ...zed_alter_leaderboard_end_time_and_more.py | 33 +++++++++++++ 2 files changed, 33 insertions(+), 48 deletions(-) delete mode 100644 scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py create mode 100644 scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py diff --git a/scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py b/scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py deleted file mode 100644 index d69a43fad..000000000 --- a/scoring/migrations/0006_leaderboard_finalized_alter_leaderboard_end_time_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-08 20:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("scoring", "0005_alter_score_aggregation_method_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="leaderboard", - name="finalized", - field=models.BooleanField( - default=False, - help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time.", - ), - ), - migrations.AlterField( - model_name="leaderboard", - name="end_time", - field=models.DateTimeField( - blank=True, - help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field.\n ", - null=True, - ), - ), - migrations.AlterField( - model_name="leaderboard", - name="finalize_time", - field=models.DateTimeField( - blank=True, - help_text="This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for:\n

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date.\n ", - null=True, - ), - ), - migrations.AlterField( - model_name="leaderboard", - name="start_time", - field=models.DateTimeField( - blank=True, - help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords.\n ", - null=True, - ), - ), - ] diff --git a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py new file mode 100644 index 000000000..7c47654c9 --- /dev/null +++ b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.4 on 2025-01-15 20:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scoring', '0006_alter_leaderboardentry_excluded_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='leaderboard', + name='finalized', + field=models.BooleanField(default=False, help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time."), + ), + migrations.AlterField( + model_name='leaderboard', + name='end_time', + field=models.DateTimeField(blank=True, help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field.\n ", null=True), + ), + migrations.AlterField( + model_name='leaderboard', + name='finalize_time', + field=models.DateTimeField(blank=True, help_text="This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for:\n

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date.\n ", null=True), + ), + migrations.AlterField( + model_name='leaderboard', + name='start_time', + field=models.DateTimeField(blank=True, help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords.\n ", null=True), + ), + ] From a311b924d4c6f5e0c2c46a4659379add1018ccd0 Mon Sep 17 00:00:00 2001 From: lsabor Date: Wed, 15 Jan 2025 12:21:20 -0800 Subject: [PATCH 4/7] format migration --- ...zed_alter_leaderboard_end_time_and_more.py | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py index 7c47654c9..d10b12ce4 100644 --- a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py +++ b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py @@ -6,28 +6,43 @@ class Migration(migrations.Migration): dependencies = [ - ('scoring', '0006_alter_leaderboardentry_excluded_and_more'), + ("scoring", "0006_alter_leaderboardentry_excluded_and_more"), ] operations = [ migrations.AddField( - model_name='leaderboard', - name='finalized', - field=models.BooleanField(default=False, help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time."), + model_name="leaderboard", + name="finalized", + field=models.BooleanField( + default=False, + help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time.", + ), ), migrations.AlterField( - model_name='leaderboard', - name='end_time', - field=models.DateTimeField(blank=True, help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field.\n ", null=True), + model_name="leaderboard", + name="end_time", + field=models.DateTimeField( + blank=True, + help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field.\n ", + null=True, + ), ), migrations.AlterField( - model_name='leaderboard', - name='finalize_time', - field=models.DateTimeField(blank=True, help_text="This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for:\n

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date.\n ", null=True), + model_name="leaderboard", + name="finalize_time", + field=models.DateTimeField( + blank=True, + help_text="This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for:\n

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date.\n ", + null=True, + ), ), migrations.AlterField( - model_name='leaderboard', - name='start_time', - field=models.DateTimeField(blank=True, help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords.\n ", null=True), + model_name="leaderboard", + name="start_time", + field=models.DateTimeField( + blank=True, + help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords.\n ", + null=True, + ), ), ] From ec012292ec0b8ad4b82631a1563a8b9468c7d84f Mon Sep 17 00:00:00 2001 From: lsabor Date: Wed, 15 Jan 2025 12:59:34 -0800 Subject: [PATCH 5/7] update help text --- ...erboard_finalized_alter_leaderboard_end_time_and_more.py | 2 +- scoring/models.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py index d10b12ce4..cc1d6d8cf 100644 --- a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py +++ b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): name="finalized", field=models.BooleanField( default=False, - help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time.", + help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to True the first time this leaderboard is updated after the finalize_time.", ), ), migrations.AlterField( diff --git a/scoring/models.py b/scoring/models.py index e6adadcac..960445c7c 100644 --- a/scoring/models.py +++ b/scoring/models.py @@ -46,7 +46,9 @@ class ScoreTypes(models.TextChoices): SPOT_BASELINE = "spot_baseline" MANUAL = "manual" - score_type = models.CharField(max_length=200, choices=ScoreTypes.choices, db_index=True) + score_type = models.CharField( + max_length=200, choices=ScoreTypes.choices, db_index=True + ) def __str__(self): return ( @@ -190,7 +192,7 @@ def get_base_score(cls, score_type: str) -> Score.ScoreTypes: ) finalized = models.BooleanField( default=False, - help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to true the first time this leaderboard is updated after the finalize_time.", + help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to True the first time this leaderboard is updated after the finalize_time.", ) def __str__(self): From e95264127369e3c15626b45af2d451da64158fd3 Mon Sep 17 00:00:00 2001 From: lsabor Date: Thu, 16 Jan 2025 13:14:09 -0800 Subject: [PATCH 6/7] add prize pool to leaderboard, make all time fields default to grab from project, improve admin documentation, create an action to assign medals and finalize --- projects/models.py | 22 ------ scoring/admin.py | 40 +++++++--- ...zed_alter_leaderboard_end_time_and_more.py | 48 ----------- ...nalized_leaderboard_prize_pool_and_more.py | 79 +++++++++++++++++++ scoring/models.py | 57 +++++++++---- scoring/utils.py | 65 +++++++++------ .../leaderboard_action_descriptions.html | 15 ++++ 7 files changed, 206 insertions(+), 120 deletions(-) delete mode 100644 scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py create mode 100644 scoring/migrations/0007_leaderboard_finalized_leaderboard_prize_pool_and_more.py create mode 100644 templates/admin/scoring/leaderboard_action_descriptions.html diff --git a/projects/models.py b/projects/models.py index 35932752c..5976cdf5f 100644 --- a/projects/models.py +++ b/projects/models.py @@ -297,28 +297,6 @@ def save(self, *args, **kwargs): raise ValueError( "Primary leaderboard must be associated with this project." ) - # If updating this project's end_date (and this isn't site_main), - # update the finalize_time of all leaderboards - if not creating: - previous = Project.objects.get(pk=self.pk) - if ( - self.close_date - and self.close_date != previous.close_date - and self.type - in ( - self.ProjectTypes.TOURNAMENT, - self.ProjectTypes.QUESTION_SERIES, - ) - ): - from scoring.models import Leaderboard - - # warn since this might not be an expected side effect - logger.warning( - f"Updating finalize_time for leaderboards of project {self.pk}" - ) - Leaderboard.objects.filter(project=self).update( - finalize_time=self.close_date - ) super().save(*args, **kwargs) if ( diff --git a/scoring/admin.py b/scoring/admin.py index 61b3e6a5c..0748b5b55 100644 --- a/scoring/admin.py +++ b/scoring/admin.py @@ -70,17 +70,21 @@ def get_queryset(self, request): @admin.register(Leaderboard) class LeaderboardAdmin(admin.ModelAdmin): + change_list_template = "admin/scoring/leaderboard_action_descriptions.html" search_fields = ["name", "project", "score_type"] - list_display = ["__str__", "id", "project", "score_type"] + list_display = ["__str__", "id", "project", "score_type", "finalized"] autocomplete_fields = ["project"] list_filter = [ AutocompleteFilterFactory("Project", "project"), + "score_type", + "finalized", ] inlines = [LeaderboardEntryInline] actions = [ "make_primary_leaderboard", "update_leaderboards", "force_update_leaderboards", + "force_finalize_and_asign_medals_leaderboards", ] def make_primary_leaderboard(self, request, queryset): @@ -95,25 +99,41 @@ def make_primary_leaderboard(self, request, queryset): messages.SUCCESS, ) - make_primary_leaderboard.short_description = ( - "Make selected leaderboards their project's primary_leaderboard" - ) + make_primary_leaderboard.short_description = "Make Primary Leaderboard" def update_leaderboards(self, request, queryset): leaderboard: Leaderboard for leaderboard in queryset: - update_project_leaderboard(leaderboard.project, leaderboard) + update_project_leaderboard( + leaderboard.project, + leaderboard, + ) - update_leaderboards.short_description = "Update selected Leaderboards" + update_leaderboards.short_description = "Update Leaderboards" def force_update_leaderboards(self, request, queryset): leaderboard: Leaderboard for leaderboard in queryset: - update_project_leaderboard(leaderboard.project, leaderboard, force=True) + update_project_leaderboard( + leaderboard.project, + leaderboard, + force_update=True, + ) + + force_update_leaderboards.short_description = "Force Update Leaderboards" + + def force_finalize_and_asign_medals_leaderboards(self, request, queryset): + leaderboard: Leaderboard + for leaderboard in queryset: + update_project_leaderboard( + leaderboard.project, + leaderboard, + force_update=True, + force_finalize=True, + ) - force_update_leaderboards.short_description = ( - "Force update selected Leaderboards. " - "Will update even if leaderboard is finalized" + force_finalize_and_asign_medals_leaderboards.short_description = ( + "Force Update, Finalize, and Assign Medals/Prizes" ) diff --git a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py b/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py deleted file mode 100644 index cc1d6d8cf..000000000 --- a/scoring/migrations/0007_leaderboard_finalized_alter_leaderboard_end_time_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-15 20:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("scoring", "0006_alter_leaderboardentry_excluded_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="leaderboard", - name="finalized", - field=models.BooleanField( - default=False, - help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to True the first time this leaderboard is updated after the finalize_time.", - ), - ), - migrations.AlterField( - model_name="leaderboard", - name="end_time", - field=models.DateTimeField( - blank=True, - help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field.\n ", - null=True, - ), - ), - migrations.AlterField( - model_name="leaderboard", - name="finalize_time", - field=models.DateTimeField( - blank=True, - help_text="This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for:\n

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date.\n ", - null=True, - ), - ), - migrations.AlterField( - model_name="leaderboard", - name="start_time", - field=models.DateTimeField( - blank=True, - help_text="This field is optional. It is used for:\n

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change.\n

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords.\n ", - null=True, - ), - ), - ] diff --git a/scoring/migrations/0007_leaderboard_finalized_leaderboard_prize_pool_and_more.py b/scoring/migrations/0007_leaderboard_finalized_leaderboard_prize_pool_and_more.py new file mode 100644 index 000000000..e59dbb7b0 --- /dev/null +++ b/scoring/migrations/0007_leaderboard_finalized_leaderboard_prize_pool_and_more.py @@ -0,0 +1,79 @@ +# Generated by Django 5.1.4 on 2025-01-16 19:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("scoring", "0006_alter_leaderboardentry_excluded_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="leaderboard", + name="finalized", + field=models.BooleanField( + default=False, + help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to True the first time this leaderboard is updated after the finalize_time.", + ), + ), + migrations.AddField( + model_name="leaderboard", + name="prize_pool", + field=models.DecimalField( + blank=True, + decimal_places=2, + default=None, + help_text="Optional. If not set, the Project's prize_pool will be used instead.\n
- If the Project has a prize pool, but this leaderboard has none, set this to 0.\n ", + max_digits=15, + null=True, + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="end_time", + field=models.DateTimeField( + blank=True, + help_text="Optional (required for global leaderboards).\n
- Global Leaderboards: filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change.\n
- Non-Global Leaderboards: has no effect on question filtering.\n
- Filtering MedalExclusionRecords: MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead - it is recommended not to use this field unless required.\n ", + null=True, + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="finalize_time", + field=models.DateTimeField( + blank=True, + help_text="Optional. If not set, the Project's close_date will be used instead.\n
- For all Leaderboards: used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).\n
- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered.\n ", + null=True, + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="score_type", + field=models.CharField( + choices=[ + ("peer_tournament", "Peer Tournament"), + ("spot_peer_tournament", "Spot Peer Tournament"), + ("relative_legacy_tournament", "Relative Legacy Tournament"), + ("baseline_global", "Baseline Global"), + ("peer_global", "Peer Global"), + ("peer_global_legacy", "Peer Global Legacy"), + ("comment_insight", "Comment Insight"), + ("question_writing", "Question Writing"), + ("manual", "Manual"), + ], + help_text="\n \n \n \n \n \n \n \n \n \n \n
peer_tournament Sum of peer scores. Most likely what you want.
spot_peer_tournament Sum of spot peer scores.
relative_legacy Old site scoring.
baseline_global Sum of baseline scores.
peer_global Coverage-weighted average of peer scores.
peer_global_legacy Average of peer scores.
comment_insight H-index of upvotes for comments on questions.
question_writing H-index of number of forecasters / 10 on questions.
manual Does not automatically update. Manually set all entries.
\n ", + max_length=200, + ), + ), + migrations.AlterField( + model_name="leaderboard", + name="start_time", + field=models.DateTimeField( + blank=True, + help_text="Optional (required for global leaderboards). If not set, the Project's open_date will be used instead.\n
- Global Leaderboards: filters for questions that have an open time after this. Automatically set, do not change.\n
- Non-Global Leaderboards: has no effect on question filtering.\n
- Filtering MedalExclusionRecords: MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered.\n ", + null=True, + ), + ), + ] diff --git a/scoring/models.py b/scoring/models.py index 7ac481cb1..2dd66762b 100644 --- a/scoring/models.py +++ b/scoring/models.py @@ -126,12 +126,12 @@ class Leaderboard(TimeStampedModel): ) class ScoreTypes(models.TextChoices): - RELATIVE_LEGACY_TOURNAMENT = "relative_legacy_tournament" - PEER_GLOBAL = "peer_global" - PEER_GLOBAL_LEGACY = "peer_global_legacy" PEER_TOURNAMENT = "peer_tournament" SPOT_PEER_TOURNAMENT = "spot_peer_tournament" + RELATIVE_LEGACY_TOURNAMENT = "relative_legacy_tournament" BASELINE_GLOBAL = "baseline_global" + PEER_GLOBAL = "peer_global" + PEER_GLOBAL_LEGACY = "peer_global_legacy" COMMENT_INSIGHT = "comment_insight" QUESTION_WRITING = "question_writing" MANUAL = "manual" @@ -162,38 +162,63 @@ def get_base_score(cls, score_type: str) -> Score.ScoreTypes: "Question Writing leaderboards do not have base scores" ) - score_type = models.CharField(max_length=200, choices=ScoreTypes.choices) + score_type = models.CharField( + max_length=200, + choices=ScoreTypes.choices, + help_text=""" + + + + + + + + + + +
peer_tournament Sum of peer scores. Most likely what you want.
spot_peer_tournament Sum of spot peer scores.
relative_legacy Old site scoring.
baseline_global Sum of baseline scores.
peer_global Coverage-weighted average of peer scores.
peer_global_legacy Average of peer scores.
comment_insight H-index of upvotes for comments on questions.
question_writing H-index of number of forecasters / 10 on questions.
manual Does not automatically update. Manually set all entries.
+ """, + ) start_time = models.DateTimeField( null=True, blank=True, - help_text="""This field is optional. It is used for: -

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered. -

- Global Leaderboards: Required for global leaderboards, filters for questions that have an open time after this. Automatically set, do not change. -

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. + help_text="""Optional (required for global leaderboards). If not set, the Project's open_date will be used instead. +
- Global Leaderboards: filters for questions that have an open time after this. Automatically set, do not change. +
- Non-Global Leaderboards: has no effect on question filtering. +
- Filtering MedalExclusionRecords: MedalExclusionRecords that have no end_time or an end_time greater than this (and a start_time before this Leaderboard's end_time or finalize_time) will be triggered. """, ) end_time = models.DateTimeField( null=True, blank=True, - help_text="""This field is optional. It is used for: -

- Filtering MedalExclusionRecords: If set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead. -

- Global Leaderboards: Required for global leaderboards, filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change. -

- Non-Global Leaderboards: has no effect on question filtering. Only used in filtering MedalExclusionRecords. Since finalize_time does the same thing in this case, it is recommended not to use this field. + help_text="""Optional (required for global leaderboards). +
- Global Leaderboards: filters for questions that have a scheduled_close_time before this (plus a grace period). Automatically set, do not change. +
- Non-Global Leaderboards: has no effect on question filtering. +
- Filtering MedalExclusionRecords: MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. If not set, this Leaderboard's finalize_time will be used instead - it is recommended not to use this field unless required. """, ) finalize_time = models.DateTimeField( null=True, blank=True, - help_text="""This field is optional, though should generally be set (exception could include a non-concluding Leaderboard like for a Tag or Topic Leaderboard). It is used for: -

- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. -

- For all Leaderboards: If set, used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized). -

- Non-Global Leaderboards: This field is automatically set to the Project's close_date. If the Project's close_date is updated, this field will be updated as well. If you want this leaderboard to have a manually set finalize_time, updating this field manually will be required after each update to the Project's close_date. + help_text="""Optional. If not set, the Project's close_date will be used instead. +
- For all Leaderboards: used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized). +
- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. """, ) finalized = models.BooleanField( default=False, help_text="If true, this Leaderboard's entries cannot be updated except by a manual action in the admin panel. Automatically set to True the first time this leaderboard is updated after the finalize_time.", ) + prize_pool = models.DecimalField( + default=None, + decimal_places=2, + max_digits=15, + null=True, + blank=True, + help_text="""Optional. If not set, the Project's prize_pool will be used instead. +
- If the Project has a prize pool, but this leaderboard has none, set this to 0. + """, + ) def __str__(self): if self.name: diff --git a/scoring/utils.py b/scoring/utils.py index 9d1fe14ff..88fddebb6 100644 --- a/scoring/utils.py +++ b/scoring/utils.py @@ -85,9 +85,12 @@ def generate_scoring_leaderboard_entries( "score_type": score_type, } - if leaderboard.finalize_time: - qs_filters["question__scheduled_close_time__lte"] = leaderboard.finalize_time - qs_filters["question__resolution_set_time__lte"] = leaderboard.finalize_time + finalize_time = leaderboard.finalize_time or ( + leaderboard.project.close_date if leaderboard.project else None + ) + if finalize_time: + qs_filters["question__scheduled_close_time__lte"] = finalize_time + qs_filters["question__resolution_set_time__lte"] = finalize_time archived_scores = ArchivedScore.objects.filter(**qs_filters).prefetch_related( "question" @@ -100,9 +103,10 @@ def generate_scoring_leaderboard_entries( aggregation_method=OuterRef("aggregation_method"), score_type=Leaderboard.ScoreTypes.get_base_score(leaderboard.score_type), ) - if leaderboard.finalize_time: + if finalize_time: archived_scores_subquery = archived_scores_subquery.filter( - question__scheduled_close_time__lte=leaderboard.finalize_time + question__scheduled_close_time__lte=finalize_time, + question__resolution_set_time__lte=finalize_time, ) calculated_scores = calculated_scores.annotate( @@ -303,20 +307,25 @@ def assign_ranks( # set up exclusions exclusion_records = MedalExclusionRecord.objects.all() - if leaderboard.start_time: + start_time = leaderboard.start_time or ( + leaderboard.project.start_date if leaderboard.project else None + ) + end_time = leaderboard.end_time or ( + leaderboard.project.close_date if leaderboard.project else None + ) + finalize_time = leaderboard.finalize_time or ( + leaderboard.project.close_date if leaderboard.project else None + ) + if start_time: exclusion_records = exclusion_records.filter( - Q(end_time__isnull=True) | Q(end_time__gte=leaderboard.start_time) + Q(end_time__isnull=True) | Q(end_time__gte=start_time) ) - if leaderboard.end_time: + if end_time: # only exclude by end_time if it's set - exclusion_records = exclusion_records.filter( - start_time__lte=leaderboard.end_time - ) - elif leaderboard.finalize_time: + exclusion_records = exclusion_records.filter(start_time__lte=end_time) + elif finalize_time: # if end_time is not set, use finalize_time - exclusion_records = exclusion_records.filter( - start_time__lte=leaderboard.finalize_time - ) + exclusion_records = exclusion_records.filter(start_time__lte=finalize_time) excluded_ids: set[int | None] = set( exclusion_records.values_list("user_id", flat=True) ) @@ -403,6 +412,7 @@ def update_project_leaderboard( project: Project | None = None, leaderboard: Leaderboard | None = None, force_update: bool = False, + force_finalize: bool = False, ) -> list[LeaderboardEntry]: if project is None and leaderboard is None: raise ValueError("Either project or leaderboard must be provided") @@ -413,12 +423,11 @@ def update_project_leaderboard( raise ValueError("Leaderboard not found") if leaderboard.score_type == Leaderboard.ScoreTypes.MANUAL: + logger.info("%s is manual, not updating", leaderboard.name) return list(leaderboard.entries.all().order_by("rank")) if not force_update and leaderboard.finalized: - logger.warning( - "Leaderboard %s is already finalized, not updating", leaderboard.name - ) + logger.warning("%s is already finalized, not updating", leaderboard.name) return list(leaderboard.entries.all().order_by("rank")) # new entries @@ -435,22 +444,30 @@ def update_project_leaderboard( new_entries = assign_prize_percentages(new_entries) # check if we're ready to finalize with medals and prizes + finalize_time = leaderboard.finalize_time or ( + project.close_date if project else None + ) if ( - ( - leaderboard.project.type + project + and ( + project.type in [ Project.ProjectTypes.SITE_MAIN, Project.ProjectTypes.TOURNAMENT, ] ) - and leaderboard.finalize_time - and (timezone.now() >= leaderboard.finalize_time) + and (force_finalize or (finalize_time and (timezone.now() >= finalize_time))) ): # assign medals new_entries = assign_medals(new_entries) # add prize if applicable - if project.prize_pool: - new_entries = assign_prizes(new_entries, project.prize_pool) + prize_pool = ( + leaderboard.prize_pool + if leaderboard.prize_pool is not None + else project.prize_pool + ) + if prize_pool: + new_entries = assign_prizes(new_entries, prize_pool) # set finalize Leaderboard.objects.filter(pk=leaderboard.pk).update(finalized=True) diff --git a/templates/admin/scoring/leaderboard_action_descriptions.html b/templates/admin/scoring/leaderboard_action_descriptions.html new file mode 100644 index 000000000..ddf025b2d --- /dev/null +++ b/templates/admin/scoring/leaderboard_action_descriptions.html @@ -0,0 +1,15 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} + +{% block object-tools %} +
+

Available Actions:

+
    +
  • Make Primary Leaderboard: Sets selected leaderboards as their project's primary leaderboard
  • +
  • Update Leaderboards: Recalculates the leaderboard - does nothing to finalized leaderboards
  • +
  • Force Update Leaderboards: Forces update on selected leaderboards even if finalized
  • +
  • Force Update, Finalize, and Assign Medals/Prizes: Forces update, finalizes, and assigns medals/prizes (if eleigble) for selected leaderboards.
  • +
+ {{ block.super }} +
+{% endblock %} \ No newline at end of file From 95ccff019ce60b2442129f55113bd27fa163ba76 Mon Sep 17 00:00:00 2001 From: lsabor Date: Thu, 16 Jan 2025 16:17:38 -0800 Subject: [PATCH 7/7] remove unused logging and inappropriate leaderboard times default setting --- projects/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/projects/models.py b/projects/models.py index 5976cdf5f..fe09343d7 100644 --- a/projects/models.py +++ b/projects/models.py @@ -1,5 +1,3 @@ -import logging - from django.db import models from django.db.models import ( Count, @@ -19,8 +17,6 @@ from users.models import User from utils.models import validate_alpha_slug, TimeStampedModel, TranslatedModel -logger = logging.getLogger(__name__) - class ProjectsQuerySet(models.QuerySet): def filter_topic(self): @@ -315,8 +311,6 @@ def save(self, *args, **kwargs): leaderboard = Leaderboard.objects.create( project=self, score_type=Leaderboard.ScoreTypes.PEER_TOURNAMENT, - start_time=self.start_date, - finalize_time=self.close_date, ) Project.objects.filter(pk=self.pk).update(primary_leaderboard=leaderboard)