Add friend
This commit is contained in:
parent
5298e6aaa5
commit
e951c761f5
12 changed files with 158 additions and 14 deletions
22
judge/migrations/0106_friend.py
Normal file
22
judge/migrations/0106_friend.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -279,3 +279,18 @@ a.edit-profile {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.follow {
|
||||||
|
background: green;
|
||||||
|
border-color: lightgreen;
|
||||||
|
}
|
||||||
|
.follow:hover {
|
||||||
|
background: darkgreen;
|
||||||
|
}
|
||||||
|
.unfollow {
|
||||||
|
background: red;
|
||||||
|
border-color: pink;
|
||||||
|
}
|
||||||
|
.unfollow:hover {
|
||||||
|
background: darkred;
|
||||||
|
}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -16,7 +16,11 @@
|
||||||
{% block title_ruler %}{% endblock %}
|
{% block title_ruler %}{% endblock %}
|
||||||
|
|
||||||
{% block title_row %}
|
{% block title_row %}
|
||||||
|
{% if request.GET.get('friend') == 'true'%}
|
||||||
|
{% set tab = 'friends' %}
|
||||||
|
{% else %}
|
||||||
{% set tab = 'list' %}
|
{% set tab = 'list' %}
|
||||||
|
{% endif %}
|
||||||
{% set title = 'Leaderboard' %}
|
{% set title = 'Leaderboard' %}
|
||||||
{% include "user/user-list-tabs.html" %}
|
{% include "user/user-list-tabs.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue