diff --git a/judge/migrations/0106_friend.py b/judge/migrations/0106_friend.py new file mode 100644 index 0000000..e820242 --- /dev/null +++ b/judge/migrations/0106_friend.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.12 on 2020-06-23 03:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0105_auto_20200523_0756'), + ] + + operations = [ + migrations.CreateModel( + name='Friend', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('current_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_users', to='judge.Profile')), + ('users', models.ManyToManyField(to='judge.Profile')), + ], + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index d780360..06bb71e 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -10,7 +10,7 @@ from judge.models.problem import LanguageLimit, License, Problem, ProblemClarifi ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ problem_directory_file -from judge.models.profile import Organization, OrganizationRequest, Profile +from judge.models.profile import Organization, OrganizationRequest, Profile, Friend from judge.models.runtime import Judge, Language, RuntimeVersion from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase from judge.models.ticket import Ticket, TicketMessage diff --git a/judge/models/profile.py b/judge/models/profile.py index 9cf046c..250dc88 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.validators import RegexValidator from django.db import models -from django.db.models import Max +from django.db.models import Max, CASCADE from django.urls import reverse from django.utils.functional import cached_property from django.utils.timezone import now @@ -16,7 +16,7 @@ from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.runtime import Language from judge.ratings import rating_class -__all__ = ['Organization', 'Profile', 'OrganizationRequest'] +__all__ = ['Organization', 'Profile', 'OrganizationRequest', 'Friend'] class EncryptedNullCharField(EncryptedCharField): @@ -178,6 +178,16 @@ class Profile(models.Model): def css_class(self): return self.get_user_css_class(self.display_rank, self.rating) + def get_friends(self): #list of usernames, including you + friend_obj = self.following_users.all() + ret = set() + + if (friend_obj): + ret = set(friend.username for friend in friend_obj[0].users.all()) + + ret.add(self.username) + return ret + class Meta: permissions = ( ('test_site', 'Shows in-progress development stuff'), @@ -202,3 +212,40 @@ class OrganizationRequest(models.Model): class Meta: verbose_name = _('organization join request') verbose_name_plural = _('organization join requests') + + +class Friend(models.Model): + users = models.ManyToManyField(Profile) + current_user = models.ForeignKey(Profile, related_name="following_users", on_delete=CASCADE) + + @classmethod + def is_friend(self, current_user, new_friend): + try: + return current_user.following_users.get().users \ + .filter(user=new_friend.user).exists() + except: + return False + + @classmethod + def make_friend(self, current_user, new_friend): + friend, created = self.objects.get_or_create( + current_user = current_user + ) + friend.users.add(new_friend) + + @classmethod + def remove_friend(self, current_user, new_friend): + friend, created = self.objects.get_or_create( + current_user = current_user + ) + friend.users.remove(new_friend) + + @classmethod + def toggle_friend(self, current_user, new_friend): + if (self.is_friend(current_user, new_friend)): + self.remove_friend(current_user, new_friend) + else: + self.make_friend(current_user, new_friend) + + def __str__(self): + return str(self.current_user) diff --git a/judge/views/contests.py b/judge/views/contests.py index d803375..8e89aad 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -602,7 +602,7 @@ def get_contest_ranking_list(request, contest, participation=None, ranking_list= problems) users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime')) - + if show_current_virtual: if participation is None and request.user.is_authenticated: participation = request.profile.current_contest diff --git a/judge/views/user.py b/judge/views/user.py index 36bdd83..421d32f 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -23,7 +23,7 @@ from django.views.generic import DetailView, ListView, TemplateView from reversion import revisions from judge.forms import ProfileForm, newsletter_id -from judge.models import Profile, Rating, Submission +from judge.models import Profile, Rating, Submission, Friend from judge.performance_points import get_pp_breakdown from judge.ratings import rating_class, rating_progress from judge.utils.problems import contest_completed_ids, user_completed_ids @@ -92,9 +92,11 @@ class UserPage(TitleMixin, UserMixin, DetailView): def get_context_data(self, **kwargs): context = super(UserPage, self).get_context_data(**kwargs) + context['followed'] = Friend.is_friend(self.request.profile, self.object) context['hide_solved'] = int(self.hide_solved) context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \ .order_by('code') + rating = self.object.ratings.order_by('-contest__end_time')[:1] context['rating'] = rating[0] if rating else None @@ -149,6 +151,19 @@ class UserAboutPage(UserPage): context['min_graph'] = min_user + ratio * delta - delta return context + # follow/unfollow user + def post(self, request, user, *args, **kwargs): + try: + if not request.profile: + raise Exception('You have to login') + if (request.profile.username == user): + raise Exception('Cannot make friend with yourself') + + following_profile = Profile.objects.get(user__username=user) + Friend.toggle_friend(request.profile, following_profile) + finally: + return HttpResponseRedirect(request.path_info) + class UserProblemsPage(UserPage): template_name = 'user/user-problems.html' @@ -269,9 +284,14 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView): default_sort = '-performance_points' def get_queryset(self): - return (Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') + ret = Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') \ .only('display_rank', 'user__username', 'points', 'rating', 'performance_points', - 'problem_count')) + 'problem_count') + + if (self.request.GET.get('friend') == 'true'): + friends = list(self.request.profile.get_friends()) + ret = ret.filter(user__username__in=friends) + return ret def get_context_data(self, **kwargs): context = super(UserList, self).get_context_data(**kwargs) diff --git a/resources/ranks.scss b/resources/ranks.scss index 3c78175..6724099 100644 --- a/resources/ranks.scss +++ b/resources/ranks.scss @@ -82,7 +82,7 @@ svg.rate-box { } .rate-master, .rate-master a { - color: #ffb100; + color: #ff8c00; } .rate-grandmaster, .rate-grandmaster a, .rate-target, .rate-target a { diff --git a/resources/users.scss b/resources/users.scss index b8e1a8c..7e64116 100644 --- a/resources/users.scss +++ b/resources/users.scss @@ -278,4 +278,19 @@ a.edit-profile { &.rate-group { color: white; } +} + +.follow { + background: green; + border-color: lightgreen; +} +.follow:hover { + background: darkgreen; +} +.unfollow { + background: red; + border-color: pink; +} +.unfollow:hover { + background: darkred; } \ No newline at end of file diff --git a/templates/contest/ranking-table.html b/templates/contest/ranking-table.html index 966640c..fd539dd 100644 --- a/templates/contest/ranking-table.html +++ b/templates/contest/ranking-table.html @@ -1,5 +1,7 @@ {% extends "user/base-users-table.html" %} +{% set friends = request.profile.get_friends() if request.user.is_authenticated else {} %} + {% block after_rank_head %} {% if has_rating %} {{ _('Rating') }} @@ -52,9 +54,7 @@ {% endblock %} {% block row_extra %} - {% if user.participation.is_disqualified %} - class="disqualified" - {% endif %} + class="{{ 'disqualified' if user.participation.is_disqualified }} {{ 'friend' if user.username in friends }} {{'highlight' if user.username == request.user.username}}" {% endblock %} {% block before_point %} diff --git a/templates/contest/ranking.html b/templates/contest/ranking.html index 6d30efd..ac3d1a2 100644 --- a/templates/contest/ranking.html +++ b/templates/contest/ranking.html @@ -286,6 +286,22 @@ $('#show-organizations-checkbox').click(function () { $('.organization-column').toggle(); }); + {% if request.user.is_authenticated %} + $('#show-friends-checkbox').click(function() { + let checked = $('#show-friends-checkbox').is(':checked'); + if (checked) { + $('tbody tr').hide(); + $('.friend').show(); + $('.friend').last().find('td').css({'border-bottom-width': + '1px', 'border-color': '#ccc'}); + } + else { + $('tr').show(); + $('.friend').last().find('td').removeAttr('style'); + } + }) + {% endif %} + highlightFirstSolve(); }); @@ -301,7 +317,12 @@ {% endif %} {% endif %} - + + + {% if request.user.is_authenticated %} + + + {% endif %} {% include "contest/ranking-table.html" %} {% endblock %} diff --git a/templates/user/list.html b/templates/user/list.html index cd9cd4b..ecd9754 100644 --- a/templates/user/list.html +++ b/templates/user/list.html @@ -16,7 +16,11 @@ {% block title_ruler %}{% endblock %} {% block title_row %} - {% set tab = 'list' %} + {% if request.GET.get('friend') == 'true'%} + {% set tab = 'friends' %} + {% else %} + {% set tab = 'list' %} + {% endif %} {% set title = 'Leaderboard' %} {% include "user/user-list-tabs.html" %} {% endblock %} diff --git a/templates/user/user-about.html b/templates/user/user-about.html index 16df95e..9a40c04 100644 --- a/templates/user/user-about.html +++ b/templates/user/user-about.html @@ -9,6 +9,20 @@ {% block user_content %}
+ {% if request.user != user.user %} +
+ {% csrf_token %} + +
+ {% endif %} {% with orgs=user.organizations.all() %} {% if orgs %}

{{ _('From') }} diff --git a/templates/user/user-list-tabs.html b/templates/user/user-list-tabs.html index e6112c6..8288c53 100644 --- a/templates/user/user-list-tabs.html +++ b/templates/user/user-list-tabs.html @@ -1,6 +1,7 @@ {% extends "tabs-base.html" %} {% block tabs %} - {{ make_tab('list', 'fa-users', url('user_list'), _('Leaderboard')) }} + {{ make_tab('list', 'fa-trophy', url('user_list'), _('Leaderboard')) }} + {{ make_tab('friends', 'fa-users', url('user_list') + '?friend=true', _('Friends')) }} {{ make_tab('organizations', 'fa-university', url('organization_list'), _('Organizations')) }} {% endblock %}