From 7ceb452cb0c2f46d3154995e6de3a88b6eec0d52 Mon Sep 17 00:00:00 2001 From: Quantum Date: Sun, 8 Aug 2021 21:44:15 -0400 Subject: [PATCH] Implement user display name override Cherry-pick from DMOJ/online-judge#1747 --- judge/admin/profile.py | 2 +- .../0167_profile_username_display_override.py | 18 ++++++++++++ judge/models/profile.py | 6 ++++ judge/views/contests.py | 3 +- judge/views/select2.py | 7 +++-- judge/views/submission.py | 28 +++++++++---------- judge/views/user.py | 2 +- templates/ticket/message.html | 2 +- templates/ticket/ticket.html | 2 +- templates/user/link.html | 2 +- 10 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 judge/migrations/0167_profile_username_display_override.py diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 68b4bb8a8..7d12add0c 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -56,7 +56,7 @@ def has_add_permission(self, request): class ProfileAdmin(NoBatchDeleteMixin, VersionAdmin): fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'allow_tagging', 'notes', - 'ban_reason', 'is_totp_enabled', 'user_script', 'current_contest') + 'username_display_override', 'ban_reason', 'is_totp_enabled', 'user_script', 'current_contest') readonly_fields = ('user',) list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full', 'date_joined', 'last_access', 'ip', 'show_public') diff --git a/judge/migrations/0167_profile_username_display_override.py b/judge/migrations/0167_profile_username_display_override.py new file mode 100644 index 000000000..ce8edcc37 --- /dev/null +++ b/judge/migrations/0167_profile_username_display_override.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-11-30 14:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0166_contest_scoreboard_cache_timeout'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='username_display_override', + field=models.CharField(blank=True, help_text='name displayed in place of username', max_length=100, verbose_name='display name override'), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index 8dea3f9b5..e7e51463e 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -177,6 +177,8 @@ class Profile(models.Model): notes = models.TextField(verbose_name=_('internal notes'), null=True, blank=True, help_text=_('Notes for administrators regarding this user.')) data_last_downloaded = models.DateTimeField(verbose_name=_('last data download time'), null=True, blank=True) + username_display_override = models.CharField(max_length=100, blank=True, verbose_name=_('display name override'), + help_text=_('name displayed in place of username')) @cached_property def organization(self): @@ -190,6 +192,10 @@ def organization(self): def username(self): return self.user.username + @cached_property + def display_name(self): + return self.username_display_override or self.username + @cached_property def has_any_solves(self): return self.submission_set.filter(points=F('problem__points')).exists() diff --git a/judge/views/contests.py b/judge/views/contests.py index 2e95d7eeb..c3e2545ee 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -677,7 +677,7 @@ def get_context_data(self, **kwargs): ContestRankingProfile = namedtuple( 'ContestRankingProfile', 'id user css_class username points cumtime tiebreaker organization participation ' - 'participation_rating problem_cells result_cell virtual', + 'participation_rating problem_cells result_cell virtual display_name', ) BestSolutionData = namedtuple('BestSolutionData', 'code points time state is_pretested') @@ -707,6 +707,7 @@ def display_user_problem(contest_problem): result_cell=contest.format.display_participation_result(participation, frozen), participation=participation, virtual=participation.virtual, + display_name=user.display_name, ) diff --git a/judge/views/select2.py b/judge/views/select2.py index d1921d455..708ae461e 100644 --- a/judge/views/select2.py +++ b/judge/views/select2.py @@ -150,18 +150,19 @@ def get(self, request, *args, **kwargs): self.gravatar_size = request.GET.get('gravatar_size', 128) self.gravatar_default = request.GET.get('gravatar_default', None) - self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank') + self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank', + 'username_display_override') context = self.get_context_data() return JsonResponse({ 'results': [ { - 'text': username, + 'text': username_override or username, 'id': username, 'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default), 'display_rank': display_rank, - } for pk, username, email, display_rank in context['object_list']], + } for pk, username, email, display_rank, username_override in context['object_list']], 'more': context['page_obj'].has_next(), }) diff --git a/judge/views/submission.py b/judge/views/submission.py index a6fd20236..8e04cbcc7 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -34,7 +34,7 @@ def submission_related(queryset): .only('id', 'user__user__username', 'user__display_rank', 'user__rating', 'problem__name', 'problem__code', 'problem__is_public', 'language__short_name', 'language__key', 'date', 'time', 'memory', 'points', 'result', 'status', 'case_points', 'case_total', 'current_testcase', 'contest_object', - 'locked_after', 'problem__submission_source_visibility_mode') \ + 'locked_after', 'problem__submission_source_visibility_mode', 'user__username_display_override') \ .prefetch_related('contest_object__authors', 'contest_object__curators') @@ -55,7 +55,7 @@ def get_title(self): submission = self.object return _('Submission of %(problem)s by %(user)s') % { 'problem': submission.problem.translated_name(self.request.LANGUAGE_CODE), - 'user': submission.user.user.username, + 'user': submission.user.display_name, } def get_content_title(self): @@ -66,7 +66,7 @@ def get_content_title(self): submission.problem.translated_name(self.request.LANGUAGE_CODE)), 'user': format_html('{1}', reverse('user_page', args=[submission.user.user.username]), - submission.user.user.username), + submission.user.display_name), }) @@ -411,7 +411,7 @@ def get_context_data(self, **kwargs): context['tab'] = 'my_submissions_tab' else: context['tab'] = 'user_submissions_tab' - context['tab_username'] = self.profile.user.username + context['tab_username'] = self.profile.display_name return context @@ -422,12 +422,12 @@ def get_queryset(self): def get_title(self): if self.is_own: return _('All my submissions') - return _('All submissions by %s') % self.username + return _('All submissions by %s') % self.profile.display_name def get_content_title(self): if self.is_own: return format_html(_('All my submissions')) - return format_html(_('All submissions by') + ' {0}', self.username, + return format_html(_('All submissions by') + ' {0}', self.profile.display_name, reverse('user_page', args=[self.username])) def get_my_submissions_page(self): @@ -535,7 +535,7 @@ def get_title(self): if self.is_own: return _('My submissions for %(problem)s') % {'problem': self.problem_name} return _("%(user)s's submissions for %(problem)s") % { - 'user': self.username, 'problem': self.problem_name, + 'user': self.profile.display_name, 'problem': self.problem_name, } def get_content_title(self): @@ -544,7 +544,7 @@ def get_content_title(self): self.username, reverse('user_page', args=[self.username]), self.problem_name, reverse('problem_detail', args=[self.problem.code])) return format_html(_("""{0}'s submissions for {2}"""), - self.username, reverse('user_page', args=[self.username]), + self.profile.display_name, reverse('user_page', args=[self.username]), self.problem_name, reverse('problem_detail', args=[self.problem.code])) def get_context_data(self, **kwargs): @@ -656,7 +656,7 @@ def get_title(self): if self.is_own: return _('My submissions in %(contest)s') % {'contest': self.contest.name} return _("%(user)s's submissions in %(contest)s") % { - 'user': self.username, + 'user': self.profile.display_name, 'contest': self.contest.name, } @@ -675,7 +675,7 @@ def get_content_title(self): return format_html(_('My submissions in {0}'), self.contest.name, reverse('contest_view', args=[self.contest.key])) return format_html(_('{0}\'s submissions in {2}'), - self.username, reverse('user_page', args=[self.username]), + self.profile.display_name, reverse('user_page', args=[self.username]), self.contest.name, reverse('contest_view', args=[self.contest.key])) def get_queryset(self): @@ -689,9 +689,9 @@ def get_queryset(self): class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): def get_title(self): if self.problem.is_accessible_by(self.request.user): - return "%s's submissions for %s in %s" % (self.username, self.problem_name, self.contest.name) + return "%s's submissions for %s in %s" % (self.profile.display_name, self.problem_name, self.contest.name) return "%s's submissions for problem %s in %s" % ( - self.username, self.get_problem_label(self.problem), self.contest.name) + self.profile.display_name, self.get_problem_label(self.problem), self.contest.name) def access_check(self, request): super(UserContestSubmissions, self).access_check(request) @@ -702,11 +702,11 @@ def get_content_title(self): if self.problem.is_accessible_by(self.request.user): return format_html(_('{0}\'s submissions for ' '{2} in {4}'), - self.username, reverse('user_page', args=[self.username]), + self.profile.display_name, reverse('user_page', args=[self.username]), self.problem_name, reverse('problem_detail', args=[self.problem.code]), self.contest.name, reverse('contest_view', args=[self.contest.key])) return format_html(_('{0}\'s submissions for ' 'problem {2} in {3}'), - self.username, reverse('user_page', args=[self.username]), + self.profile.display_name, reverse('user_page', args=[self.username]), self.get_problem_label(self.problem), self.contest.name, reverse('contest_view', args=[self.contest.key])) diff --git a/judge/views/user.py b/judge/views/user.py index 5ea0040e8..4628850a5 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -103,7 +103,7 @@ def dispatch(self, request, *args, **kwargs): def get_title(self): return (_('My account') if self.request.user == self.object.user else - _('User %s') % self.object.user.username) + _('User %s') % self.object.display_name) # TODO: the same code exists in problem.py, maybe move to problems.py? @cached_property diff --git a/templates/ticket/message.html b/templates/ticket/message.html index 0762cfc16..c0d0bebc5 100644 --- a/templates/ticket/message.html +++ b/templates/ticket/message.html @@ -2,7 +2,7 @@
-
{{ message.user.user.username }}
+
{{ message.user.display_name }}
diff --git a/templates/ticket/ticket.html b/templates/ticket/ticket.html index 7330d37e9..b698bcd41 100644 --- a/templates/ticket/ticket.html +++ b/templates/ticket/ticket.html @@ -366,7 +366,7 @@
diff --git a/templates/user/link.html b/templates/user/link.html index f0a73bb39..777f38848 100644 --- a/templates/user/link.html +++ b/templates/user/link.html @@ -1 +1 @@ -{{ user.username }} +{{ profile.display_name }}