Add friend

This commit is contained in:
cuom1999 2020-06-23 20:46:33 -05:00
parent 5298e6aaa5
commit e951c761f5
12 changed files with 158 additions and 14 deletions

View file

@ -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')),
],
),
]

View file

@ -10,7 +10,7 @@ from judge.models.problem import LanguageLimit, License, Problem, ProblemClarifi
ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet
from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
problem_directory_file 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.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
from judge.models.ticket import Ticket, TicketMessage from judge.models.ticket import Ticket, TicketMessage

View file

@ -4,7 +4,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models 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.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now 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.models.runtime import Language
from judge.ratings import rating_class from judge.ratings import rating_class
__all__ = ['Organization', 'Profile', 'OrganizationRequest'] __all__ = ['Organization', 'Profile', 'OrganizationRequest', 'Friend']
class EncryptedNullCharField(EncryptedCharField): class EncryptedNullCharField(EncryptedCharField):
@ -178,6 +178,16 @@ class Profile(models.Model):
def css_class(self): def css_class(self):
return self.get_user_css_class(self.display_rank, self.rating) 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: class Meta:
permissions = ( permissions = (
('test_site', 'Shows in-progress development stuff'), ('test_site', 'Shows in-progress development stuff'),
@ -202,3 +212,40 @@ class OrganizationRequest(models.Model):
class Meta: class Meta:
verbose_name = _('organization join request') verbose_name = _('organization join request')
verbose_name_plural = _('organization join requests') 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)

View file

@ -602,7 +602,7 @@ def get_contest_ranking_list(request, contest, participation=None, ranking_list=
problems) problems)
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime')) users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime'))
if show_current_virtual: if show_current_virtual:
if participation is None and request.user.is_authenticated: if participation is None and request.user.is_authenticated:
participation = request.profile.current_contest participation = request.profile.current_contest

View file

@ -23,7 +23,7 @@ from django.views.generic import DetailView, ListView, TemplateView
from reversion import revisions from reversion import revisions
from judge.forms import ProfileForm, newsletter_id 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.performance_points import get_pp_breakdown
from judge.ratings import rating_class, rating_progress from judge.ratings import rating_class, rating_progress
from judge.utils.problems import contest_completed_ids, user_completed_ids 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): def get_context_data(self, **kwargs):
context = super(UserPage, self).get_context_data(**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['hide_solved'] = int(self.hide_solved)
context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \ context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \
.order_by('code') .order_by('code')
rating = self.object.ratings.order_by('-contest__end_time')[:1] rating = self.object.ratings.order_by('-contest__end_time')[:1]
context['rating'] = rating[0] if rating else None context['rating'] = rating[0] if rating else None
@ -149,6 +151,19 @@ class UserAboutPage(UserPage):
context['min_graph'] = min_user + ratio * delta - delta context['min_graph'] = min_user + ratio * delta - delta
return context 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): class UserProblemsPage(UserPage):
template_name = 'user/user-problems.html' template_name = 'user/user-problems.html'
@ -269,9 +284,14 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
default_sort = '-performance_points' default_sort = '-performance_points'
def get_queryset(self): 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', .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): def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs) context = super(UserList, self).get_context_data(**kwargs)

View file

@ -82,7 +82,7 @@ svg.rate-box {
} }
.rate-master, .rate-master a { .rate-master, .rate-master a {
color: #ffb100; color: #ff8c00;
} }
.rate-grandmaster, .rate-grandmaster a, .rate-target, .rate-target a { .rate-grandmaster, .rate-grandmaster a, .rate-target, .rate-target a {

View file

@ -278,4 +278,19 @@ a.edit-profile {
&.rate-group { &.rate-group {
color: white; color: white;
} }
}
.follow {
background: green;
border-color: lightgreen;
}
.follow:hover {
background: darkgreen;
}
.unfollow {
background: red;
border-color: pink;
}
.unfollow:hover {
background: darkred;
} }

View file

@ -1,5 +1,7 @@
{% extends "user/base-users-table.html" %} {% extends "user/base-users-table.html" %}
{% set friends = request.profile.get_friends() if request.user.is_authenticated else {} %}
{% block after_rank_head %} {% block after_rank_head %}
{% if has_rating %} {% if has_rating %}
<th>{{ _('Rating') }}</th> <th>{{ _('Rating') }}</th>
@ -52,9 +54,7 @@
{% endblock %} {% endblock %}
{% block row_extra %} {% block row_extra %}
{% if user.participation.is_disqualified %} class="{{ 'disqualified' if user.participation.is_disqualified }} {{ 'friend' if user.username in friends }} {{'highlight' if user.username == request.user.username}}"
class="disqualified"
{% endif %}
{% endblock %} {% endblock %}
{% block before_point %} {% block before_point %}

View file

@ -286,6 +286,22 @@
$('#show-organizations-checkbox').click(function () { $('#show-organizations-checkbox').click(function () {
$('.organization-column').toggle(); $('.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(); highlightFirstSolve();
}); });
@ -301,7 +317,12 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<input id="show-organizations-checkbox" type="checkbox" style="vertical-align: bottom"> <input id="show-organizations-checkbox" type="checkbox" style="vertical-align: bottom">
<label for="show-organizations-checkbox" style="vertical-align: bottom">{{ _('Show organizations') }}</label> <label for="show-organizations-checkbox" style="vertical-align: bottom; margin-right: 1em;">{{ _('Show organizations') }}</label>
{% if request.user.is_authenticated %}
<input id="show-friends-checkbox" type="checkbox" style="vertical-align: bottom;">
<label for="show-friends-checkbox" style="vertical-align: bottom;">{{ _('Show friends only') }}</label>
{% endif %}
</div> </div>
{% include "contest/ranking-table.html" %} {% include "contest/ranking-table.html" %}
{% endblock %} {% endblock %}

View file

@ -16,7 +16,11 @@
{% block title_ruler %}{% endblock %} {% block title_ruler %}{% endblock %}
{% block title_row %} {% block title_row %}
{% set tab = 'list' %} {% if request.GET.get('friend') == 'true'%}
{% set tab = 'friends' %}
{% else %}
{% set tab = 'list' %}
{% endif %}
{% set title = 'Leaderboard' %} {% set title = 'Leaderboard' %}
{% include "user/user-list-tabs.html" %} {% include "user/user-list-tabs.html" %}
{% endblock %} {% endblock %}

View file

@ -9,6 +9,20 @@
{% block user_content %} {% block user_content %}
<div class="content-description"> <div class="content-description">
{% if request.user != user.user %}
<form method="post">
{% csrf_token %}
<button class="{{ 'unfollow' if followed else 'follow' }}">
{% if followed %}
<i class="fa fa-remove"></i>
{{ _('Unfollow') }}
{% else %}
<i class="fa fa-user-plus"></i>
{{ _('Follow') }}
{% endif %}
</button>
</form>
{% endif %}
{% with orgs=user.organizations.all() %} {% with orgs=user.organizations.all() %}
{% if orgs %} {% if orgs %}
<p style="margin-top: 0"><b>{{ _('From') }}</b> <p style="margin-top: 0"><b>{{ _('From') }}</b>

View file

@ -1,6 +1,7 @@
{% extends "tabs-base.html" %} {% extends "tabs-base.html" %}
{% block tabs %} {% 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')) }} {{ make_tab('organizations', 'fa-university', url('organization_list'), _('Organizations')) }}
{% endblock %} {% endblock %}