Merge from master

This commit is contained in:
cuom1999 2021-12-29 17:28:49 +07:00
commit 7e6cc57c65
227 changed files with 82565 additions and 17221 deletions

1
.gitignore vendored
View file

@ -15,4 +15,5 @@ sass_processed
<desired bridge log path> <desired bridge log path>
node_modules/ node_modules/
package-lock.json package-lock.json
/src

View file

@ -1,12 +0,0 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
django.setup()
application = get_default_application()

View file

@ -1,76 +0,0 @@
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from .models import Message
from django.urls import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.core import serializers
from judge.jinja2.gravatar import gravatar
from judge.models.profile import Profile
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = 'room'
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name,
)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name,
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
author = self.scope['user']
author = Profile.objects.get(user=author)
message['author'] = author.username
message['css_class'] = author.css_class
message['image'] = gravatar(author, 32)
message_saved = save_data_and_return(message, author)
message['time'] = message_saved[0]['fields']['time']
message['id'] = message_saved[0]['pk']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
},
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message,
}))
# return time
def save_data_and_return(message, author):
new_message = Message(body=message['body'],
author=author,
)
new_message.save()
json_data = serializers.serialize("json",
Message.objects
.filter(pk=new_message.id)
)
return json.loads(json_data)

View file

@ -0,0 +1,28 @@
# Generated by Django 2.2.17 on 2021-10-11 00:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('judge', '0116_auto_20211011_0645'),
('chat_box', '0004_auto_20200505_2336'),
]
operations = [
migrations.CreateModel(
name='Room',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_one', to='judge.Profile', verbose_name='user 1')),
('user_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_two', to='judge.Profile', verbose_name='user 2')),
],
),
migrations.AddField(
model_name='message',
name='room',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id'),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2.17 on 2021-11-12 05:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('judge', '0116_auto_20211011_0645'),
('chat_box', '0005_auto_20211011_0714'),
]
operations = [
migrations.CreateModel(
name='UserRoom',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_seen', models.DateTimeField(verbose_name='last seen')),
('room', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2021-11-12 05:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('chat_box', '0006_userroom'),
]
operations = [
migrations.AlterField(
model_name='userroom',
name='last_seen',
field=models.DateTimeField(auto_now_add=True, verbose_name='last seen'),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.2.17 on 2021-11-18 10:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('judge', '0116_auto_20211011_0645'),
('chat_box', '0007_auto_20211112_1255'),
]
operations = [
migrations.CreateModel(
name='Ignore',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ignored_users', models.ManyToManyField(to='judge.Profile')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ignored_chat_users', to='judge.Profile', verbose_name='user')),
],
),
]

View file

@ -1,5 +1,3 @@
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.db import models from django.db import models
from django.db.models import CASCADE from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -10,12 +8,23 @@ from judge.models.profile import Profile
__all__ = ['Message'] __all__ = ['Message']
class Room(models.Model):
user_one = models.ForeignKey(Profile, related_name="user_one", verbose_name='user 1', on_delete=CASCADE)
user_two = models.ForeignKey(Profile, related_name="user_two", verbose_name='user 2', on_delete=CASCADE)
def contain(self, profile):
return self.user_one == profile or self.user_two == profile
def other_user(self, profile):
return self.user_one if profile == self.user_two else self.user_two
def users(self):
return [self.user_one, self.user_two]
class Message(models.Model): class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
body = models.TextField(verbose_name=_('body of comment'), max_length=8192) body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
hidden = models.BooleanField(verbose_name='is hidden', default=False) hidden = models.BooleanField(verbose_name='is hidden', default=False)
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
new_message = self.id new_message = self.id
@ -27,3 +36,49 @@ class Message(models.Model):
verbose_name = 'message' verbose_name = 'message'
verbose_name_plural = 'messages' verbose_name_plural = 'messages'
ordering = ('-time',) ordering = ('-time',)
class UserRoom(models.Model):
user = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True)
last_seen = models.DateTimeField(verbose_name=_('last seen'), auto_now_add=True)
class Ignore(models.Model):
user = models.ForeignKey(Profile, related_name="ignored_chat_users", verbose_name=_('user'), on_delete=CASCADE)
ignored_users = models.ManyToManyField(Profile)
@classmethod
def is_ignored(self, current_user, new_friend):
try:
return current_user.ignored_chat_users.get().ignored_users \
.filter(id=new_friend.id).exists()
except:
return False
@classmethod
def get_ignored_users(self, user):
try:
return self.objects.get(user=user).ignored_users.all()
except Ignore.DoesNotExist:
return Profile.objects.none()
@classmethod
def add_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(
user = current_user
)
ignore.ignored_users.add(friend)
@classmethod
def remove_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(
user = current_user
)
ignore.ignored_users.remove(friend)
@classmethod
def toggle_ignore(self, current_user, friend):
if (self.is_ignored(current_user, friend)):
self.remove_ignore(current_user, friend)
else:
self.add_ignore(current_user, friend)

View file

@ -1,8 +0,0 @@
from django.urls import re_path
from . import consumers
ASGI_APPLICATION = "chat_box.routing.application"
websocket_urlpatterns = [
re_path(r'ws/chat/', consumers.ChatConsumer),
]

18
chat_box/utils.py Normal file
View file

@ -0,0 +1,18 @@
from cryptography.fernet import Fernet
from django.conf import settings
secret_key = settings.CHAT_SECRET_KEY
fernet = Fernet(secret_key)
def encrypt_url(creator_id, other_id):
message = str(creator_id) + '_' + str(other_id)
return fernet.encrypt(message.encode()).decode()
def decrypt_url(message_encrypted):
try:
dec_message = fernet.decrypt(message_encrypted.encode()).decode()
creator_id, other_id = dec_message.split('_')
return int(creator_id), int(other_id)
except Exception as e:
return None, None

View file

@ -1,79 +1,99 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView from django.views.generic import ListView
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect, HttpResponseRedirect
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.exceptions import PermissionDenied
from django.shortcuts import render from django.shortcuts import render
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.db.models import Case, BooleanField, When, Q, Subquery, OuterRef, Exists, Count, IntegerField
from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django.urls import reverse
import datetime
from judge import event_poster as event
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
from .models import Message, Profile from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
from chat_box.utils import encrypt_url, decrypt_url
import json import json
def format_messages(messages):
msg_list = [{
'time': msg.time,
'author': msg.author,
'body': msg.body,
'image': gravatar(msg.author, 32),
'id': msg.id,
'css_class': msg.author.css_class,
} for msg in messages]
return json.dumps(msg_list, default=str)
def get_admin_online_status():
all_admin = Profile.objects.filter(display_rank='admin')
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5)
ret = []
for admin in all_admin:
is_online = False
if (admin.last_access >= last_five_minutes):
is_online = True
ret.append({'user': admin, 'is_online': is_online})
return ret
class ChatView(ListView): class ChatView(ListView):
model = Message
context_object_name = 'message' context_object_name = 'message'
template_name = 'chat/chat.html' template_name = 'chat/chat.html'
title = _('Chat Box') title = _('Chat Box')
paginate_by = 50
paginator = Paginator(Message.objects.filter(hidden=False), paginate_by) def __init__(self):
super().__init__()
self.room_id = None
self.room = None
self.paginate_by = 50
self.messages = None
self.paginator = None
def get_queryset(self): def get_queryset(self):
return Message.objects.filter(hidden=False) return self.messages
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request_room = kwargs['room_id']
page = request.GET.get('page') page = request.GET.get('page')
if (page == None):
# return render(request, 'chat/chat.html', {'message': format_messages(Message.objects.all())}) if request_room:
try:
self.room = Room.objects.get(id=request_room)
if not can_access_room(request, self.room):
return HttpResponseBadRequest()
except Room.DoesNotExist:
return HttpResponseBadRequest()
else:
request_room = None
if request_room != self.room_id or not self.messages:
self.room_id = request_room
self.messages = Message.objects.filter(hidden=False, room=self.room_id)
self.paginator = Paginator(self.messages, self.paginate_by)
if page == None:
update_last_seen(request, **kwargs)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
cur_page = self.paginator.get_page(page) cur_page = self.paginator.get_page(page)
return HttpResponse(format_messages(cur_page.object_list))
return render(request, 'chat/message_list.html', {
'object_list': cur_page.object_list,
'num_pages': self.paginator.num_pages
})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# hard code, should be fixed later
address = f'{self.request.get_host()}/ws/chat/'
if self.request.is_secure():
context['ws_address'] = f'wss://{address}'
else:
context['ws_address'] = f'ws://{address}'
context['title'] = self.title context['title'] = self.title
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5) context['last_msg'] = event.last()
context['online_users'] = Profile.objects \ context['status_sections'] = get_status_context(self.request)
.filter(display_rank='user', context['room'] = self.room_id
last_access__gte = last_five_minutes)\ context['unread_count_lobby'] = get_unread_count(None, self.request.profile)
.order_by('-rating') if self.room:
context['admin_status'] = get_admin_online_status() users_room = [self.room.user_one, self.room.user_two]
users_room.remove(self.request.profile)
context['other_user'] = users_room[0]
context['other_online'] = get_user_online_status(context['other_user'])
context['is_ignored'] = Ignore.is_ignored(self.request.profile, context['other_user'])
else:
context['online_count'] = get_online_count()
context['message_template'] = {
'author': self.request.profile,
'id': '$id',
'time': timezone.now(),
'body': '$body'
}
return context return context
def delete_message(request): def delete_message(request):
ret = {'delete': 'done'} ret = {'delete': 'done'}
@ -81,17 +101,352 @@ def delete_message(request):
return JsonResponse(ret) return JsonResponse(ret)
if request.user.is_staff: if request.user.is_staff:
messid = int(request.POST.get('messid')) try:
all_mess = Message.objects.all() messid = int(request.POST.get('message'))
mess = Message.objects.get(id=messid)
except:
return HttpResponseBadRequest()
for mess in all_mess: mess.hidden = True
if mess.id == messid: mess.save()
mess.hidden = True
mess.save()
new_elt = {'time': mess.time, 'content': mess.body}
ret = new_elt
break
return JsonResponse(ret) return JsonResponse(ret)
return JsonResponse(ret) return JsonResponse(ret)
@login_required
def post_message(request):
ret = {'msg': 'posted'}
if request.method != 'POST':
return HttpResponseBadRequest()
room = None
if request.POST['room']:
room = Room.objects.get(id=request.POST['room'])
if not can_access_room(request, room) or request.profile.mute:
return HttpResponseBadRequest()
new_message = Message(author=request.profile,
body=request.POST['body'],
room=room)
new_message.save()
if not room:
event.post('chat_lobby', {
'type': 'lobby',
'author_id': request.profile.id,
'message': new_message.id,
'room': 'None',
'tmp_id': request.POST.get('tmp_id')
})
else:
for user in room.users():
event.post('chat_' + str(user.id), {
'type': 'private',
'author_id': request.profile.id,
'message': new_message.id,
'room': room.id,
'tmp_id': request.POST.get('tmp_id')
})
return JsonResponse(ret)
def can_access_room(request, room):
return not room or room.user_one == request.profile or room.user_two == request.profile
@login_required
def chat_message_ajax(request):
if request.method != 'GET':
return HttpResponseBadRequest()
try:
message_id = request.GET['message']
except KeyError:
return HttpResponseBadRequest()
try:
message = Message.objects.filter(hidden=False).get(id=message_id)
room = message.room
if room and not room.contain(request.profile):
return HttpResponse('Unauthorized', status=401)
except Message.DoesNotExist:
return HttpResponseBadRequest()
return render(request, 'chat/message.html', {
'message': message,
})
@login_required
def update_last_seen(request, **kwargs):
if 'room_id' in kwargs:
room_id = kwargs['room_id']
elif request.method == 'GET':
room_id = request.GET.get('room')
elif request.method == 'POST':
room_id = request.POST.get('room')
else:
return HttpResponseBadRequest()
try:
profile = request.profile
room = None
if room_id:
room = Room.objects.get(id=int(room_id))
except Room.DoesNotExist:
return HttpResponseBadRequest()
except Exception as e:
return HttpResponseBadRequest()
if room and not room.contain(profile):
return HttpResponseBadRequest()
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
user_room.last_seen = timezone.now()
user_room.save()
return JsonResponse({'msg': 'updated'})
def get_online_count():
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
return Profile.objects.filter(last_access__gte=last_two_minutes).count()
def get_user_online_status(user):
time_diff = timezone.now() - user.last_access
is_online = time_diff <= timezone.timedelta(minutes=2)
return is_online
def user_online_status_ajax(request):
if request.method != 'GET':
return HttpResponseBadRequest()
user_id = request.GET.get('user')
if user_id:
try:
user_id = int(user_id)
user = Profile.objects.get(id=user_id)
except Exception as e:
return HttpResponseBadRequest()
is_online = get_user_online_status(user)
return render(request, 'chat/user_online_status.html', {
'other_user': user,
'other_online': is_online,
'is_ignored': Ignore.is_ignored(request.profile, user)
})
else:
return render(request, 'chat/user_online_status.html', {
'online_count': get_online_count(),
})
def get_online_status(request_user, queryset, rooms=None):
if not queryset:
return None
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
ret = []
if rooms:
unread_count = get_unread_count(rooms, request_user)
count = {}
for i in unread_count:
count[i['other_user']] = i['unread_count']
for user in queryset:
is_online = False
if (user.last_access >= last_two_minutes):
is_online = True
user_dict = {'user': user, 'is_online': is_online}
if rooms and user.id in count:
user_dict['unread_count'] = count[user.id]
user_dict['url'] = encrypt_url(request_user.id, user.id)
ret.append(user_dict)
return ret
def get_status_context(request, include_ignored=False):
if include_ignored:
ignored_users = Profile.objects.none()
queryset = Profile.objects
else:
ignored_users = Ignore.get_ignored_users(request.profile)
queryset = Profile.objects.exclude(id__in=ignored_users)
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
recent_profile = Room.objects.filter(
Q(user_one=request.profile) | Q(user_two=request.profile)
).annotate(
last_msg_time=Subquery(
Message.objects.filter(room=OuterRef('pk')).values('time')[:1]
),
other_user=Case(
When(user_one=request.profile, then='user_two'),
default='user_one',
)
).filter(last_msg_time__isnull=False)\
.exclude(other_user__in=ignored_users)\
.order_by('-last_msg_time').values('other_user', 'id')[:20]
recent_profile_id = [str(i['other_user']) for i in recent_profile]
joined_id = ','.join(recent_profile_id)
recent_rooms = [int(i['id']) for i in recent_profile]
recent_list = None
if joined_id:
recent_list = Profile.objects.raw(
f'SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})')
friend_list = Friend.get_friend_profiles(request.profile).exclude(id__in=recent_profile_id)\
.exclude(id__in=ignored_users)\
.order_by('-last_access')
admin_list = queryset.filter(display_rank='admin')\
.exclude(id__in=friend_list).exclude(id__in=recent_profile_id)
all_user_status = queryset\
.filter(display_rank='user',
last_access__gte = last_two_minutes)\
.annotate(is_online=Case(default=True,output_field=BooleanField()))\
.order_by('-rating').exclude(id__in=friend_list).exclude(id__in=admin_list)\
.exclude(id__in=recent_profile_id)[:30]
return [
{
'title': 'Recent',
'user_list': get_online_status(request.profile, recent_list, recent_rooms),
},
{
'title': 'Following',
'user_list': get_online_status(request.profile, friend_list),
},
{
'title': 'Admin',
'user_list': get_online_status(request.profile, admin_list),
},
{
'title': 'Other',
'user_list': get_online_status(request.profile, all_user_status),
},
]
@login_required
def online_status_ajax(request):
return render(request, 'chat/online_status.html', {
'status_sections': get_status_context(request),
'unread_count_lobby': get_unread_count(None, request.profile),
})
@login_required
def get_room(user_one, user_two):
if user_one.id > user_two.id:
user_one, user_two = user_two, user_one
room, created = Room.objects.get_or_create(user_one=user_one, user_two=user_two)
return room
@login_required
def get_or_create_room(request):
if request.method == 'GET':
decrypted_other_id = request.GET.get('other')
elif request.method == 'POST':
decrypted_other_id = request.POST.get('other')
else:
return HttpResponseBadRequest()
request_id, other_id = decrypt_url(decrypted_other_id)
if not other_id or not request_id or request_id != request.profile.id:
return HttpResponseBadRequest()
try:
other_user = Profile.objects.get(id=int(other_id))
except Exception:
return HttpResponseBadRequest()
user = request.profile
if not other_user or not user:
return HttpResponseBadRequest()
# TODO: each user can only create <= 300 rooms
room = get_room(other_user, user)
for u in [other_user, user]:
user_room, _ = UserRoom.objects.get_or_create(user=u, room=room)
user_room.last_seen = timezone.now()
user_room.save()
if request.method == 'GET':
return JsonResponse({'room': room.id, 'other_user_id': other_user.id})
return HttpResponseRedirect(reverse('chat', kwargs={'room_id': room.id}))
def get_unread_count(rooms, user):
if rooms:
mess = Message.objects.filter(room=OuterRef('room'),
time__gte=OuterRef('last_seen'))\
.exclude(author=user)\
.order_by().values('room')\
.annotate(unread_count=Count('pk')).values('unread_count')
return UserRoom.objects\
.filter(user=user, room__in=rooms)\
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
other_user=Case(
When(room__user_one=user, then='room__user_two'),
default='room__user_one',
)
).filter(unread_count__gte=1).values('other_user', 'unread_count')
else: # lobby
mess = Message.objects.filter(room__isnull=True,
time__gte=OuterRef('last_seen'))\
.exclude(author=user)\
.order_by().values('room')\
.annotate(unread_count=Count('pk')).values('unread_count')
res = UserRoom.objects\
.filter(user=user, room__isnull=True)\
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
).values_list('unread_count', flat=True)
return res[0] if len(res) else 0
@login_required
def toggle_ignore(request, **kwargs):
user_id = kwargs['user_id']
if not user_id:
return HttpResponseBadRequest()
try:
other_user = Profile.objects.get(id=user_id)
except:
return HttpResponseBadRequest()
Ignore.toggle_ignore(request.profile, other_user)
next_url = request.GET.get('next', '/')
return HttpResponseRedirect(next_url)
@login_required
def get_unread_boxes(request):
if (request.method != 'GET'):
return HttpResponseBadRequest()
mess = Message.objects.filter(room=OuterRef('room'),
time__gte=OuterRef('last_seen'))\
.exclude(author=request.profile)\
.order_by().values('room')\
.annotate(unread_count=Count('pk')).values('unread_count')
unread_boxes = UserRoom.objects\
.filter(user=request.profile, room__isnull=False)\
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
).filter(unread_count__gte=1).count()
return JsonResponse({'unread_boxes': unread_boxes})

View file

@ -1,12 +0,0 @@
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat_box.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat_box.routing.websocket_urlpatterns
)
),
})

View file

@ -72,7 +72,7 @@ DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
DMOJ_BLOG_NEW_CONTEST_COUNT = 7 DMOJ_BLOG_NEW_CONTEST_COUNT = 7
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7 DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3 DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
DMOJ_PDF_PROBLEM_CACHE = '' DMOJ_PDF_PROBLEM_CACHE = ''
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir() DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
@ -125,6 +125,10 @@ SLIMERJS_PAPER_SIZE = 'Letter'
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer' PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer'
PUPPETEER_PAPER_SIZE = 'Letter' PUPPETEER_PAPER_SIZE = 'Letter'
USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = 'chromedriver'
PYGMENT_THEME = 'pygment-github.css' PYGMENT_THEME = 'pygment-github.css'
INLINE_JQUERY = True INLINE_JQUERY = True
INLINE_FONTAWESOME = True INLINE_FONTAWESOME = True
@ -239,8 +243,8 @@ INSTALLED_APPS += (
'impersonate', 'impersonate',
'django_jinja', 'django_jinja',
'chat_box', 'chat_box',
'channels',
'newsletter', 'newsletter',
'django.forms',
) )
MIDDLEWARE = ( MIDDLEWARE = (
@ -263,6 +267,8 @@ MIDDLEWARE = (
'django.contrib.redirects.middleware.RedirectFallbackMiddleware', 'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
) )
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
IMPERSONATE_REQUIRE_SUPERUSER = True IMPERSONATE_REQUIRE_SUPERUSER = True
IMPERSONATE_DISABLE_LOGGING = True IMPERSONATE_DISABLE_LOGGING = True
@ -484,6 +490,8 @@ SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.user.user_details', 'social_core.pipeline.user.user_details',
) )
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['first_name', 'last_name']
SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ['email', 'username']
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email'] SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email']
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_SLUGIFY_USERNAMES = True SOCIAL_AUTH_SLUGIFY_USERNAMES = True
@ -495,11 +503,6 @@ MOSS_API_KEY = None
CELERY_WORKER_HIJACK_ROOT_LOGGER = False CELERY_WORKER_HIJACK_ROOT_LOGGER = False
try:
with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f:
exec(f.read(), globals())
except IOError:
pass
TESTCASE_VISIBLE_LENGTH = 64 TESTCASE_VISIBLE_LENGTH = 64
@ -509,17 +512,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
MESSAGES_TO_LOAD = 15 MESSAGES_TO_LOAD = 15
ASGI_APPLICATION = 'dmoj.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('0.0.0.0', 6379)],
},
},
}
NEWSLETTER_CONFIRM_EMAIL = False NEWSLETTER_CONFIRM_EMAIL = False
# Amount of seconds to wait between each email. Here 100ms is used. # Amount of seconds to wait between each email. Here 100ms is used.
@ -529,4 +521,13 @@ NEWSLETTER_EMAIL_DELAY = 0.1
NEWSLETTER_BATCH_DELAY = 60 NEWSLETTER_BATCH_DELAY = 60
# Number of emails in one batch # Number of emails in one batch
NEWSLETTER_BATCH_SIZE = 100 NEWSLETTER_BATCH_SIZE = 100
# Google form to request name
REGISTER_NAME_URL = None
try:
with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f:
exec(f.read(), globals())
except IOError:
pass

View file

@ -1,4 +1,5 @@
from chat_box.views import ChatView, delete_message from chat_box.views import *
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
@ -21,10 +22,10 @@ from judge.views import TitledTemplateView, about, api, blog, comment, contests,
notification, organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, \ notification, organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, \
ticket, totp, user, widgets ticket, totp, user, widgets
from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \
problem_data_file, problem_init_view problem_data_file, problem_init_view, ProblemZipUploadView
from judge.views.register import ActivationView, RegistrationView from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \ from judge.views.select2 import AssigneeSelect2View, ChatUserSearchSelect2View, CommentSelect2View, \
ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \ ContestSelect2View, ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
UserSearchSelect2View, UserSelect2View UserSearchSelect2View, UserSelect2View
admin.autodiscover() admin.autodiscover()
@ -115,6 +116,7 @@ urlpatterns = [
url(r'^problem/(?P<problem>[^/]+)', include([ url(r'^problem/(?P<problem>[^/]+)', include([
url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'),
url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'), url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'),
url(r'^/comments$', problem.ProblemComments.as_view(), name='problem_comments'),
url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'), url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'),
url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'), url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'),
url(r'^/pdf/(?P<language>[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'), url(r'^/pdf/(?P<language>[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'),
@ -131,6 +133,7 @@ urlpatterns = [
url(r'^/test_data$', ProblemDataView.as_view(), name='problem_data'), url(r'^/test_data$', ProblemDataView.as_view(), name='problem_data'),
url(r'^/test_data/init$', problem_init_view, name='problem_data_init'), url(r'^/test_data/init$', problem_init_view, name='problem_data_init'),
url(r'^/test_data/diff$', ProblemSubmissionDiff.as_view(), name='problem_submission_diff'), url(r'^/test_data/diff$', ProblemSubmissionDiff.as_view(), name='problem_submission_diff'),
url(r'^/test_data/upload$', ProblemZipUploadView.as_view(), name='problem_zip_upload'),
url(r'^/data/(?P<path>.+)$', problem_data_file, name='problem_data_file'), url(r'^/data/(?P<path>.+)$', problem_data_file, name='problem_data_file'),
url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'),
@ -225,6 +228,9 @@ urlpatterns = [
url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(), url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(),
name='contest_participation_disqualify'), name='contest_participation_disqualify'),
url(r'^/clarification$', contests.NewContestClarificationView.as_view(), name='new_contest_clarification'),
url(r'^/clarification/ajax$', contests.ContestClarificationAjax.as_view(), name='contest_clarification_ajax'),
url(r'^/$', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))), url(r'^/$', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))),
])), ])),
@ -284,6 +290,7 @@ urlpatterns = [
url(r'^select2/', include([ url(r'^select2/', include([
url(r'^user_search$', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'), url(r'^user_search$', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'),
url(r'^user_search_chat$', ChatUserSearchSelect2View.as_view(), name='chat_user_search_select2_ajax'),
url(r'^contest_users/(?P<contest>\w+)$', ContestUserSearchSelect2View.as_view(), url(r'^contest_users/(?P<contest>\w+)$', ContestUserSearchSelect2View.as_view(),
name='contest_user_search_select2_ajax'), name='contest_user_search_select2_ajax'),
url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'), url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'),
@ -369,16 +376,28 @@ urlpatterns = [
url(r'^custom_checker_sample/', about.custom_checker_sample, name='custom_checker_sample'), url(r'^custom_checker_sample/', about.custom_checker_sample, name='custom_checker_sample'),
url(r'^chat/', include([ url(r'^chat/', include([
url(r'^$', url(r'^(?P<room_id>\d*)$', login_required(ChatView.as_view()), name='chat'),
login_required(ChatView.as_view()), url(r'^delete/$', delete_message, name='delete_chat_message'),
name='chat'), url(r'^post/$', post_message, name='post_chat_message'),
url(r'^delete/$', delete_message, name='delete_message') url(r'^ajax$', chat_message_ajax, name='chat_message_ajax'),
url(r'^online_status/ajax$', online_status_ajax, name='online_status_ajax'),
url(r'^get_or_create_room$', get_or_create_room, name='get_or_create_room'),
url(r'^update_last_seen$', update_last_seen, name='update_last_seen'),
url(r'^online_status/user/ajax$', user_online_status_ajax, name='user_online_status_ajax'),
url(r'^toggle_ignore/(?P<user_id>\d+)$', toggle_ignore, name='toggle_ignore'),
url(r'^get_unread_boxes$', get_unread_boxes, name='get_unread_boxes'),
])), ])),
url(r'^notifications/', url(r'^notifications/',
login_required(notification.NotificationList.as_view()), login_required(notification.NotificationList.as_view()),
name='notification') name='notification'),
url(r'^import_users/', include([
url(r'^$', user.ImportUsersView.as_view(), name='import_users'),
url(r'post_file/$', user.import_users_post_file, name='import_users_post_file'),
url(r'submit/$', user.import_users_submit, name='import_users_submit'),
url(r'sample/$', user.sample_import_users, name='import_users_sample')
])),
] ]
favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png', favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png',
@ -389,7 +408,7 @@ favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png',
'favicon-96x96.png', 'favicon-96x96.png',
'favicon-32x32.png', 'favicon-16x16.png', 'android-chrome-192x192.png', 'android-chrome-48x48.png', 'favicon-32x32.png', 'favicon-16x16.png', 'android-chrome-192x192.png', 'android-chrome-48x48.png',
'mstile-310x150.png', 'apple-touch-icon-144x144.png', 'browserconfig.xml', 'manifest.json', 'mstile-310x150.png', 'apple-touch-icon-144x144.png', 'browserconfig.xml', 'manifest.json',
'apple-touch-icon-120x120.png', 'mstile-310x310.png'] 'apple-touch-icon-120x120.png', 'mstile-310x310.png', 'reload.png']
for favicon in favicon_paths: for favicon in favicon_paths:
urlpatterns.append(url(r'^%s$' % favicon, RedirectView.as_view( urlpatterns.append(url(r'^%s$' % favicon, RedirectView.as_view(

View file

@ -7,10 +7,12 @@ from django.forms import ModelForm, ModelMultipleChoiceField
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from django_ace import AceWidget
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest from judge.ratings import rate_contest
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
@ -94,7 +96,9 @@ class ContestForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2', 'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}), attrs={'style': 'width: 100%'}),
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'), 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
@ -111,18 +115,19 @@ class ContestForm(ModelForm):
class ContestAdmin(VersionAdmin): class ContestAdmin(VersionAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('key', 'name', 'organizers')}), (None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard', (_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'scoreboard_visibility',
'run_pretests_only', 'points_precision')}), 'run_pretests_only', 'points_precision')}),
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}), (_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}), (_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
(_('Format'), {'fields': ('format_name', 'format_config')}), (_('Format'), {'fields': ('format_name', 'format_config', 'problem_label_script')}),
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}), (_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private', (_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
'organizations', 'view_contest_scoreboard')}), 'organizations', 'view_contest_scoreboard')}),
(_('Justice'), {'fields': ('banned_users',)}), (_('Justice'), {'fields': ('banned_users',)}),
) )
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count') list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
search_fields = ('key', 'name')
inlines = [ContestProblemInline] inlines = [ContestProblemInline]
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
@ -146,7 +151,7 @@ class ContestAdmin(VersionAdmin):
if request.user.has_perm('judge.edit_all_contest'): if request.user.has_perm('judge.edit_all_contest'):
return queryset return queryset
else: else:
return queryset.filter(organizers__id=request.profile.id) return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct()
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
readonly = [] readonly = []
@ -158,6 +163,8 @@ class ContestAdmin(VersionAdmin):
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations'] readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
if not request.user.has_perm('judge.change_contest_visibility'): if not request.user.has_perm('judge.change_contest_visibility'):
readonly += ['is_visible'] readonly += ['is_visible']
if not request.user.has_perm('judge.contest_problem_label'):
readonly += ['problem_label_script']
return readonly return readonly
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
@ -185,9 +192,9 @@ class ContestAdmin(VersionAdmin):
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_contest'): if not request.user.has_perm('judge.edit_own_contest'):
return False return False
if request.user.has_perm('judge.edit_all_contest') or obj is None: if obj is None:
return True return True
return obj.organizers.filter(id=request.profile.id).exists() return obj.is_editable_by(request.user)
def _rescore(self, contest_key): def _rescore(self, contest_key):
from judge.tasks import rescore_contest from judge.tasks import rescore_contest
@ -232,14 +239,10 @@ class ContestAdmin(VersionAdmin):
if not request.user.has_perm('judge.contest_rating'): if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied() raise PermissionDenied()
with transaction.atomic(): with transaction.atomic():
if connection.vendor == 'sqlite': with connection.cursor() as cursor:
Rating.objects.all().delete()
else:
cursor = connection.cursor()
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
cursor.close()
Profile.objects.update(rating=None) Profile.objects.update(rating=None)
for contest in Contest.objects.filter(is_rated=True).order_by('end_time'): for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'):
rate_contest(contest) rate_contest(contest)
return HttpResponseRedirect(reverse('admin:judge_contest_changelist')) return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
@ -247,16 +250,21 @@ class ContestAdmin(VersionAdmin):
if not request.user.has_perm('judge.contest_rating'): if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied() raise PermissionDenied()
contest = get_object_or_404(Contest, id=id) contest = get_object_or_404(Contest, id=id)
if not contest.is_rated: if not contest.is_rated or not contest.ended:
raise Http404() raise Http404()
with transaction.atomic(): with transaction.atomic():
contest.rate() contest.rate()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist'))) return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
def get_form(self, *args, **kwargs): def get_form(self, request, obj=None, **kwargs):
form = super(ContestAdmin, self).get_form(*args, **kwargs) form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
if 'problem_label_script' in form.base_fields:
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
# on the model.
form.base_fields['problem_label_script'].widget = AceWidget('lua', request.profile.ace_theme)
perms = ('edit_own_contest', 'edit_all_contest') perms = ('edit_own_contest', 'edit_all_contest')
form.base_fields['organizers'].queryset = Profile.objects.filter( form.base_fields['curators'].queryset = Profile.objects.filter(
Q(user__is_superuser=True) | Q(user__is_superuser=True) |
Q(user__groups__permissions__codename__in=perms) | Q(user__groups__permissions__codename__in=perms) |
Q(user__user_permissions__codename__in=perms), Q(user__user_permissions__codename__in=perms),
@ -274,7 +282,7 @@ class ContestParticipationForm(ModelForm):
class ContestParticipationAdmin(admin.ModelAdmin): class ContestParticipationAdmin(admin.ModelAdmin):
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime') list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker')
actions = ['recalculate_results'] actions = ['recalculate_results']
actions_on_bottom = actions_on_top = True actions_on_bottom = actions_on_top = True
search_fields = ('contest__key', 'contest__name', 'user__user__username') search_fields = ('contest__key', 'contest__name', 'user__user__username')
@ -284,7 +292,7 @@ class ContestParticipationAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
return super(ContestParticipationAdmin, self).get_queryset(request).only( return super(ContestParticipationAdmin, self).get_queryset(request).only(
'contest__name', 'contest__format_name', 'contest__format_config', 'contest__name', 'contest__format_name', 'contest__format_config',
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual', 'user__user__username', 'real_start', 'score', 'cumtime', 'tiebreaker', 'virtual',
) )
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):

View file

@ -30,7 +30,7 @@ class JudgeAppConfig(AppConfig):
from django.contrib.auth.models import User from django.contrib.auth.models import User
try: try:
lang = Language.get_python3() lang = Language.get_default_language()
for user in User.objects.filter(profile=None): for user in User.objects.filter(profile=None):
# These poor profileless users # These poor profileless users
profile = Profile(user=user, language=lang) profile = Profile(user=user, language=lang)

View file

@ -80,8 +80,10 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
return self.comment_page return self.comment_page
def is_comment_locked(self): def is_comment_locked(self):
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and if self.request.user.has_perm('judge.override_comment_lock'):
not self.request.user.has_perm('judge.override_comment_lock')) return False
return (CommentLock.objects.filter(page=self.get_comment_page()).exists()
or (self.request.in_contest and self.request.participation.contest.use_clarifications))
@method_decorator(login_required) @method_decorator(login_required)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):

View file

@ -1,5 +1,6 @@
from judge.contest_format.atcoder import AtCoderContestFormat from judge.contest_format.atcoder import AtCoderContestFormat
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.ecoo import ECOOContestFormat from judge.contest_format.ecoo import ECOOContestFormat
from judge.contest_format.icpc import ICPCContestFormat
from judge.contest_format.ioi import IOIContestFormat from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.registry import choices, formats from judge.contest_format.registry import choices, formats

View file

@ -91,6 +91,7 @@ class AtCoderContestFormat(DefaultContestFormat):
participation.cumtime = cumtime + penalty participation.cumtime = cumtime + penalty
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -82,6 +82,14 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
""" """
raise NotImplementedError() raise NotImplementedError()
@abstractmethod
def get_contest_problem_label_script(self):
"""
Returns the default Lua script to generate contest problem labels.
:return: A string, the Lua script.
"""
raise NotImplementedError()
@classmethod @classmethod
def best_solution_state(cls, points, total): def best_solution_state(cls, points, total):
if not points: if not points:

View file

@ -41,6 +41,7 @@ class DefaultContestFormat(BaseContestFormat):
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
@ -68,3 +69,10 @@ class DefaultContestFormat(BaseContestFormat):
def get_problem_breakdown(self, participation, contest_problems): def get_problem_breakdown(self, participation, contest_problems):
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems] return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]
def get_contest_problem_label_script(self):
return '''
function(n)
return tostring(math.floor(n + 1))
end
'''

View file

@ -92,6 +92,7 @@ class ECOOContestFormat(DefaultContestFormat):
participation.cumtime = cumtime participation.cumtime = cumtime
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -0,0 +1,129 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import connection
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr
@register_contest_format('icpc')
class ICPCContestFormat(DefaultContestFormat):
name = gettext_lazy('ICPC')
config_defaults = {'penalty': 20}
config_validators = {'penalty': lambda x: x >= 0}
'''
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20.
'''
@classmethod
def validate(cls, config):
if config is None:
return
if not isinstance(config, dict):
raise ValidationError('ICPC-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
raise ValidationError('unknown config key "%s"' % key)
if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key)
if not cls.config_validators[key](value):
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
def __init__(self, contest, config):
self.config = self.config_defaults.copy()
self.config.update(config or {})
self.contest = contest
def update_participation(self, participation):
cumtime = 0
last = 0
penalty = 0
score = 0
format_data = {}
with connection.cursor() as cursor:
cursor.execute('''
SELECT MAX(cs.points) as `points`, (
SELECT MIN(csub.date)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
judge_submission csub ON (csub.id = ccs.submission_id)
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
) AS `time`, cp.id AS `prob`
FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id)
GROUP BY cp.id
''', (participation.id, participation.id))
for points, time, prob in cursor.fetchall():
time = from_database_time(time)
dt = (time - participation.start).total_seconds()
# Compute penalty
if self.config['penalty']:
# An IE can have a submission result of `None`
subs = participation.submissions.exclude(submission__result__isnull=True) \
.exclude(submission__result__in=['IE', 'CE']) \
.filter(problem_id=prob)
if points:
prev = subs.filter(submission__date__lte=time).count() - 1
penalty += prev * self.config['penalty'] * 60
else:
# We should always display the penalty, even if the user has a score of 0
prev = subs.count()
else:
prev = 0
if points:
cumtime += dt
last = max(last, dt)
format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev}
score += points
participation.cumtime = max(0, cumtime + penalty)
participation.score = score
participation.tiebreaker = last # field is sorted from least to greatest
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data:
penalty = format_html('<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
return format_html(
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
self.best_solution_state(format_data['points'], contest_problem.points)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
penalty=penalty,
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
else:
return mark_safe('<td></td>')
def get_contest_problem_label_script(self):
return '''
function(n)
n = n + 1
ret = ""
while n > 0 do
ret = string.char((n - 1) % 26 + 65) .. ret
n = math.floor((n - 1) / 26)
end
return ret
end
'''

View file

@ -73,6 +73,7 @@ class IOIContestFormat(DefaultContestFormat):
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -27,11 +27,12 @@ else:
def wrap(self, source, outfile): def wrap(self, source, outfile):
return self._wrap_div(self._wrap_pre(_wrap_code(source))) return self._wrap_div(self._wrap_pre(_wrap_code(source)))
def highlight_code(code, language, cssclass='codehilite'): def highlight_code(code, language, cssclass='codehilite', linenos=True):
try: try:
lexer = pygments.lexers.get_lexer_by_name(language) lexer = pygments.lexers.get_lexer_by_name(language)
except pygments.util.ClassNotFound: except pygments.util.ClassNotFound:
return _make_pre_code(code) return _make_pre_code(code)
# return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table'))) if linenos:
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table')))
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass))) return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)))

View file

@ -8,7 +8,7 @@ from statici18n.templatetags.statici18n import inlinei18n
from judge.highlight_code import highlight_code from judge.highlight_code import highlight_code
from judge.user_translations import gettext from judge.user_translations import gettext
from . import (camo, datetime, filesize, gravatar, language, markdown, rating, reference, render, social, from . import (camo, chat, datetime, filesize, gravatar, language, markdown, rating, reference, render, social,
spaceless, submission, timedelta) spaceless, submission, timedelta)
from . import registry from . import registry

View file

@ -1,9 +1,8 @@
from judge.utils.camo import client as camo_client from judge.utils.camo import client as camo_client
from . import registry from . import registry
@registry.filter @registry.filter
def camo(url): def camo(url):
if camo_client is None: if camo_client is None:
return url return url
return camo_client.rewrite_url(url) return camo_client.rewrite_url(url)

6
judge/jinja2/chat.py Normal file
View file

@ -0,0 +1,6 @@
from . import registry
from chat_box.utils import encrypt_url
@registry.function
def chat_param(request_profile, profile):
return encrypt_url(request_profile.id, profile.id)

View file

@ -12,6 +12,7 @@ from lxml.etree import ParserError, XMLSyntaxError
from judge.highlight_code import highlight_code from judge.highlight_code import highlight_code
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
from judge.jinja2.markdown.spoiler import SpoilerInlineGrammar, SpoilerInlineLexer, SpoilerRenderer
from judge.utils.camo import client as camo_client from judge.utils.camo import client as camo_client
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
from .. import registry from .. import registry
@ -26,15 +27,15 @@ class CodeSafeInlineGrammar(mistune.InlineGrammar):
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word* emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
class AwesomeInlineGrammar(MathInlineGrammar, CodeSafeInlineGrammar): class AwesomeInlineGrammar(MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar):
pass pass
class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer): class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexer):
grammar_class = AwesomeInlineGrammar grammar_class = AwesomeInlineGrammar
class AwesomeRenderer(MathRenderer, mistune.Renderer): class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nofollow = kwargs.pop('nofollow', True) self.nofollow = kwargs.pop('nofollow', True)
self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None
@ -128,7 +129,6 @@ def markdown(value, style, math_engine=None, lazy_load=False):
markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer, markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer,
parse_block_html=1, parse_inline_html=1) parse_block_html=1, parse_inline_html=1)
result = markdown(value) result = markdown(value)
if post_processors: if post_processors:
try: try:
tree = html.fromstring(result, parser=html.HTMLParser(recover=True)) tree = html.fromstring(result, parser=html.HTMLParser(recover=True))

View file

@ -64,4 +64,4 @@ class MathRenderer(mistune.Renderer):
def math(self, math): def math(self, math):
if self.mathoid is None or not math: if self.mathoid is None or not math:
return r'\(%s\)' % mistune.escape(str(math)) return r'\(%s\)' % mistune.escape(str(math))
return self.mathoid.inline_math(math) return self.mathoid.inline_math(math)

View file

@ -0,0 +1,27 @@
import re
import mistune
class SpoilerInlineGrammar(mistune.InlineGrammar):
spoiler = re.compile(r'^\|\|(.+?)\s+([\s\S]+?)\s*\|\|')
class SpoilerInlineLexer(mistune.InlineLexer):
grammar_class = SpoilerInlineGrammar
def __init__(self, *args, **kwargs):
self.default_rules.insert(0, 'spoiler')
super(SpoilerInlineLexer, self).__init__(*args, **kwargs)
def output_spoiler(self, m):
return self.renderer.spoiler(m.group(1), m.group(2))
class SpoilerRenderer(mistune.Renderer):
def spoiler(self, summary, text):
return '''<details>
<summary style="color: brown">
<span class="spoiler-summary">%s</span>
</summary>
<div class="spoiler-text">%s</div>
</details>''' % (summary, text)

View file

@ -24,5 +24,7 @@ def seconds(timedelta):
@registry.filter @registry.filter
@registry.render_with('time-remaining-fragment.html') @registry.render_with('time-remaining-fragment.html')
def as_countdown(timedelta): def as_countdown(time):
return {'countdown': timedelta} time_now = datetime.datetime.now(datetime.timezone.utc)
initial = abs(time - time_now)
return {'countdown': time, 'initial': initial}

View file

@ -8,8 +8,8 @@ from django.template.loader import get_template
from django.utils import translation from django.utils import translation
from judge.models import Problem, ProblemTranslation from judge.models import Problem, ProblemTranslation
from judge.pdf_problems import DefaultPdfMaker, PhantomJSPdfMaker, PuppeteerPDFRender, SlimerJSPdfMaker from judge.pdf_problems import DefaultPdfMaker, PhantomJSPdfMaker, PuppeteerPDFRender, SeleniumPDFRender, \
SlimerJSPdfMaker
class Command(BaseCommand): class Command(BaseCommand):
help = 'renders a PDF file of a problem' help = 'renders a PDF file of a problem'
@ -24,6 +24,7 @@ class Command(BaseCommand):
parser.add_argument('-s', '--slimerjs', action='store_const', const=SlimerJSPdfMaker, dest='engine') parser.add_argument('-s', '--slimerjs', action='store_const', const=SlimerJSPdfMaker, dest='engine')
parser.add_argument('-c', '--chrome', '--puppeteer', action='store_const', parser.add_argument('-c', '--chrome', '--puppeteer', action='store_const',
const=PuppeteerPDFRender, dest='engine') const=PuppeteerPDFRender, dest='engine')
parser.add_argument('-S', '--selenium', action='store_const', const=SeleniumPDFRender, dest='engine')
def handle(self, *args, **options): def handle(self, *args, **options):
try: try:

View file

@ -6,6 +6,11 @@ from django.db import migrations, models
import judge.models.runtime import judge.models.runtime
def create_python3(apps, schema_editor):
Language = apps.get_model('judge', 'Language')
Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0]
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@ -13,6 +18,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(create_python3, reverse_code=migrations.RunPython.noop),
migrations.AlterField( migrations.AlterField(
model_name='profile', model_name='profile',
name='language', name='language',

View file

@ -0,0 +1,63 @@
# Generated by Django 2.2.17 on 2021-05-24 19:22
from django.db import migrations, models
def hide_scoreboard_eq_true(apps, schema_editor):
Contest = apps.get_model('judge', 'Contest')
Contest.objects.filter(hide_scoreboard=True).update(scoreboard_visibility='C')
def scoreboard_visibility_eq_contest(apps, schema_editor):
Contest = apps.get_model('judge', 'Contest')
Contest.objects.filter(scoreboard_visibility__in=('C', 'P')).update(hide_scoreboard=True)
class Migration(migrations.Migration):
dependencies = [
('judge', '0114_auto_20201228_1041'),
]
operations = [
migrations.AlterModelOptions(
name='contest',
options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('change_contest_visibility', 'Change contest visibility'), ('contest_problem_label', 'Edit contest problem label script')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
),
migrations.RemoveField(
model_name='contest',
name='hide_scoreboard',
),
migrations.RemoveField(
model_name='contest',
name='organizers',
),
migrations.AddField(
model_name='contest',
name='authors',
field=models.ManyToManyField(help_text='These users will be able to edit the contest.', related_name='_contest_authors_+', to='judge.Profile'),
),
migrations.AddField(
model_name='contest',
name='curators',
field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the contest, but will not be listed as authors.', related_name='_contest_curators_+', to='judge.Profile'),
),
migrations.AddField(
model_name='contest',
name='problem_label_script',
field=models.TextField(blank=True, help_text='A custom Lua function to generate problem labels. Requires a single function with an integer parameter, the zero-indexed contest problem index, and returns a string, the label.', verbose_name='contest problem label script'),
),
migrations.AddField(
model_name='contest',
name='scoreboard_visibility',
field=models.CharField(choices=[('V', 'Visible'), ('C', 'Hidden for duration of contest'), ('P', 'Hidden for duration of participation')], default='V', help_text='Scoreboard visibility through the duration of the contest', max_length=1, verbose_name='scoreboard visibility'),
),
migrations.AddField(
model_name='contest',
name='testers',
field=models.ManyToManyField(blank=True, help_text='These users will be able to view the contest, but not edit it.', related_name='_contest_testers_+', to='judge.Profile'),
),
migrations.AddField(
model_name='contestparticipation',
name='tiebreaker',
field=models.FloatField(default=0.0, verbose_name='tie-breaking field'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2021-10-10 23:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0115_auto_20210525_0222'),
]
operations = [
migrations.AlterField(
model_name='contest',
name='format_name',
field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('icpc', 'ICPC'), ('ioi', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'),
),
]

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,208 @@
import math
from operator import attrgetter, itemgetter
from django.db import migrations, models
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.utils import timezone
def tie_ranker(iterable, key=attrgetter('points')):
rank = 0
delta = 1
last = None
buf = []
for item in iterable:
new = key(item)
if new != last:
for _ in buf:
yield rank + (delta - 1) / 2.0
rank += delta
delta = 0
buf = []
delta += 1
buf.append(item)
last = key(item)
for _ in buf:
yield rank + (delta - 1) / 2.0
def rational_approximation(t):
# Abramowitz and Stegun formula 26.2.23.
# The absolute value of the error should be less than 4.5 e-4.
c = [2.515517, 0.802853, 0.010328]
d = [1.432788, 0.189269, 0.001308]
numerator = (c[2] * t + c[1]) * t + c[0]
denominator = ((d[2] * t + d[1]) * t + d[0]) * t + 1.0
return t - numerator / denominator
def normal_CDF_inverse(p):
assert 0.0 < p < 1
# See article above for explanation of this section.
if p < 0.5:
# F^-1(p) = - G^-1(p)
return -rational_approximation(math.sqrt(-2.0 * math.log(p)))
else:
# F^-1(p) = G^-1(1-p)
return rational_approximation(math.sqrt(-2.0 * math.log(1.0 - p)))
def WP(RA, RB, VA, VB):
return (math.erf((RB - RA) / math.sqrt(2 * (VA * VA + VB * VB))) + 1) / 2.0
def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is_disqualified):
# actual_rank: 1 is first place, N is last place
# if there are ties, use the average of places (if places 2, 3, 4, 5 tie, use 3.5 for all of them)
N = len(old_rating)
new_rating = old_rating[:]
new_volatility = old_volatility[:]
if N <= 1:
return new_rating, new_volatility
ranking = list(range(N))
ranking.sort(key=old_rating.__getitem__, reverse=True)
ave_rating = float(sum(old_rating)) / N
sum1 = sum(i * i for i in old_volatility) / N
sum2 = sum((i - ave_rating) ** 2 for i in old_rating) / (N - 1)
CF = math.sqrt(sum1 + sum2)
for i in range(N):
ERank = 0.5
for j in range(N):
ERank += WP(old_rating[i], old_rating[j], old_volatility[i], old_volatility[j])
EPerf = -normal_CDF_inverse((ERank - 0.5) / N)
APerf = -normal_CDF_inverse((actual_rank[i] - 0.5) / N)
PerfAs = old_rating[i] + CF * (APerf - EPerf)
Weight = 1.0 / (1 - (0.42 / (times_rated[i] + 1) + 0.18)) - 1.0
if old_rating[i] > 2500:
Weight *= 0.8
elif old_rating[i] >= 2000:
Weight *= 0.9
Cap = 150.0 + 1500.0 / (times_rated[i] + 2)
new_rating[i] = (old_rating[i] + Weight * PerfAs) / (1.0 + Weight)
if abs(old_rating[i] - new_rating[i]) > Cap:
if old_rating[i] < new_rating[i]:
new_rating[i] = old_rating[i] + Cap
else:
new_rating[i] = old_rating[i] - Cap
if times_rated[i] == 0:
new_volatility[i] = 385
else:
new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight +
(old_volatility[i] ** 2) / (Weight + 1))
if is_disqualified[i]:
# DQed users can manipulate TopCoder ratings to get higher volatility in order to increase their rating
# later on, prohibit this by ensuring their volatility never increases in this situation
new_volatility[i] = min(new_volatility[i], old_volatility[i])
# try to keep the sum of ratings constant
adjust = float(sum(old_rating) - sum(new_rating)) / N
new_rating = list(map(adjust.__add__, new_rating))
# inflate a little if we have to so people who placed first don't lose rating
best_rank = min(actual_rank)
for i in range(N):
if abs(actual_rank[i] - best_rank) <= 1e-3 and new_rating[i] < old_rating[i] + 1:
new_rating[i] = old_rating[i] + 1
return list(map(int, map(round, new_rating))), list(map(int, map(round, new_volatility)))
def tc_rate_contest(contest, Rating, Profile):
rating_subquery = Rating.objects.filter(user=OuterRef('user'))
rating_sorted = rating_subquery.order_by('-contest__end_time')
users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \
.annotate(submissions=Count('submission'),
last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), 1200),
volatility=Coalesce(Subquery(rating_sorted.values('volatility')[:1]), 535),
times=Coalesce(Subquery(rating_subquery.order_by().values('user_id')
.annotate(count=Count('id')).values('count')), 0)) \
.exclude(user_id__in=contest.rate_exclude.all()) \
.filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker', 'is_disqualified',
'last_rating', 'volatility', 'times')
if not contest.rate_all:
users = users.filter(submissions__gt=0)
if contest.rating_floor is not None:
users = users.exclude(last_rating__lt=contest.rating_floor)
if contest.rating_ceiling is not None:
users = users.exclude(last_rating__gt=contest.rating_ceiling)
users = list(users)
participation_ids = list(map(itemgetter('id'), users))
user_ids = list(map(itemgetter('user_id'), users))
is_disqualified = list(map(itemgetter('is_disqualified'), users))
ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
old_rating = list(map(itemgetter('last_rating'), users))
old_volatility = list(map(itemgetter('volatility'), users))
times_ranked = list(map(itemgetter('times'), users))
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked, is_disqualified)
now = timezone.now()
ratings = [Rating(user_id=i, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z)
for i, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)]
Rating.objects.bulk_create(ratings)
Profile.objects.filter(contest_history__contest=contest, contest_history__virtual=0).update(
rating=Subquery(Rating.objects.filter(user=OuterRef('id'))
.order_by('-contest__end_time').values('rating')[:1]))
# inspired by rate_all_view
def rate_tc(apps, schema_editor):
Contest = apps.get_model('judge', 'Contest')
Rating = apps.get_model('judge', 'Rating')
Profile = apps.get_model('judge', 'Profile')
with schema_editor.connection.cursor() as cursor:
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
Profile.objects.update(rating=None)
for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'):
tc_rate_contest(contest, Rating, Profile)
# inspired by rate_all_view
def rate_elo_mmr(apps, schema_editor):
Rating = apps.get_model('judge', 'Rating')
Profile = apps.get_model('judge', 'Profile')
with schema_editor.connection.cursor() as cursor:
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
Profile.objects.update(rating=None)
# Don't populate Rating
class Migration(migrations.Migration):
dependencies = [
('judge', '0117_auto_20211209_0612'),
]
operations = [
migrations.RunPython(migrations.RunPython.noop, rate_tc, atomic=True),
migrations.AddField(
model_name='rating',
name='mean',
field=models.FloatField(verbose_name='raw rating'),
),
migrations.AddField(
model_name='rating',
name='performance',
field=models.FloatField(verbose_name='contest performance'),
),
migrations.RemoveField(
model_name='rating',
name='volatility',
field=models.IntegerField(verbose_name='volatility'),
),
migrations.RunPython(rate_elo_mmr, migrations.RunPython.noop, atomic=True),
]

View file

@ -105,7 +105,7 @@ class Comment(MPTTModel):
try: try:
link = None link = None
if self.page.startswith('p:'): if self.page.startswith('p:'):
link = reverse('problem_detail', args=(self.page[2:],)) link = reverse('problem_comments', args=(self.page[2:],))
elif self.page.startswith('c:'): elif self.page.startswith('c:'):
link = reverse('contest_view', args=(self.page[2:],)) link = reverse('contest_view', args=(self.page[2:],))
elif self.page.startswith('b:'): elif self.page.startswith('b:'):

View file

@ -1,12 +1,13 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models, transaction from django.db import models, transaction
from django.db.models import CASCADE from django.db.models import CASCADE, Q
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from jsonfield import JSONField from jsonfield import JSONField
from lupa import LuaRuntime
from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON, MOSS_LANG_PASCAL from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON, MOSS_LANG_PASCAL
from judge import contest_format from judge import contest_format
@ -48,11 +49,25 @@ class ContestTag(models.Model):
class Contest(models.Model): class Contest(models.Model):
SCOREBOARD_VISIBLE = 'V'
SCOREBOARD_AFTER_CONTEST = 'C'
SCOREBOARD_AFTER_PARTICIPATION = 'P'
SCOREBOARD_VISIBILITY = (
(SCOREBOARD_VISIBLE, _('Visible')),
(SCOREBOARD_AFTER_CONTEST, _('Hidden for duration of contest')),
(SCOREBOARD_AFTER_PARTICIPATION, _('Hidden for duration of participation')),
)
key = models.CharField(max_length=20, verbose_name=_('contest id'), unique=True, key = models.CharField(max_length=20, verbose_name=_('contest id'), unique=True,
validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))]) validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
name = models.CharField(max_length=100, verbose_name=_('contest name'), db_index=True) name = models.CharField(max_length=100, verbose_name=_('contest name'), db_index=True)
organizers = models.ManyToManyField(Profile, help_text=_('These people will be able to edit the contest.'), authors = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest.'),
related_name='organizers+') related_name='authors+')
curators = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest, '
'but will not be listed as authors.'),
related_name='curators+', blank=True)
testers = models.ManyToManyField(Profile, help_text=_('These users will be able to view the contest, '
'but not edit it.'),
blank=True, related_name='testers+')
description = models.TextField(verbose_name=_('description'), blank=True) description = models.TextField(verbose_name=_('description'), blank=True)
problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem') problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem')
start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True) start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True)
@ -64,10 +79,9 @@ class Contest(models.Model):
'specified organizations.')) 'specified organizations.'))
is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'), is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'),
default=False) default=False)
hide_scoreboard = models.BooleanField(verbose_name=_('hide scoreboard'), scoreboard_visibility = models.CharField(verbose_name=_('scoreboard visibility'), default=SCOREBOARD_VISIBLE,
help_text=_('Whether the scoreboard should remain hidden for the duration ' max_length=1, help_text=_('Scoreboard visibility through the duration '
'of the contest.'), 'of the contest'), choices=SCOREBOARD_VISIBILITY)
default=False)
view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True, view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True,
related_name='view_contest_scoreboard', related_name='view_contest_scoreboard',
help_text=_('These users will be able to view the scoreboard.')) help_text=_('These users will be able to view the scoreboard.'))
@ -116,6 +130,10 @@ class Contest(models.Model):
help_text=_('A JSON object to serve as the configuration for the chosen contest format ' help_text=_('A JSON object to serve as the configuration for the chosen contest format '
'module. Leave empty to use None. Exact format depends on the contest format ' 'module. Leave empty to use None. Exact format depends on the contest format '
'selected.')) 'selected.'))
problem_label_script = models.TextField(verbose_name='contest problem label script', blank=True,
help_text='A custom Lua function to generate problem labels. Requires a '
'single function with an integer parameter, the zero-indexed '
'contest problem index, and returns a string, the label.')
points_precision = models.IntegerField(verbose_name=_('precision points'), default=2, points_precision = models.IntegerField(verbose_name=_('precision points'), default=2,
validators=[MinValueValidator(0), MaxValueValidator(10)], validators=[MinValueValidator(0), MaxValueValidator(10)],
help_text=_('Number of digits to round points to.')) help_text=_('Number of digits to round points to.'))
@ -128,30 +146,72 @@ class Contest(models.Model):
def format(self): def format(self):
return self.format_class(self, self.format_config) return self.format_class(self, self.format_config)
@cached_property
def get_label_for_problem(self):
def DENY_ALL(obj, attr_name, is_setting):
raise AttributeError()
lua = LuaRuntime(attribute_filter=DENY_ALL, register_eval=False, register_builtins=False)
return lua.eval(self.problem_label_script or self.format.get_contest_problem_label_script())
def clean(self): def clean(self):
# Django will complain if you didn't fill in start_time or end_time, so we don't have to. # Django will complain if you didn't fill in start_time or end_time, so we don't have to.
if self.start_time and self.end_time and self.start_time >= self.end_time: if self.start_time and self.end_time and self.start_time >= self.end_time:
raise ValidationError('What is this? A contest that ended before it starts?') raise ValidationError('What is this? A contest that ended before it starts?')
self.format_class.validate(self.format_config) self.format_class.validate(self.format_config)
try:
# a contest should have at least one problem, with contest problem index 0
# so test it to see if the script returns a valid label.
label = self.get_label_for_problem(0)
except Exception as e:
raise ValidationError('Contest problem label script: %s' % e)
else:
if not isinstance(label, str):
raise ValidationError('Contest problem label script: script should return a string.')
def is_in_contest(self, user): def is_in_contest(self, user):
if user.is_authenticated: if user.is_authenticated:
profile = user.profile profile = user.profile
return profile and profile.current_contest is not None and profile.current_contest.contest == self return profile and profile.current_contest is not None and profile.current_contest.contest == self
return False return False
def can_see_scoreboard(self, user): def can_see_own_scoreboard(self, user):
if user.has_perm('judge.see_private_contest'): if self.can_see_full_scoreboard(user):
return True return True
if user.is_authenticated and self.organizers.filter(id=user.profile.id).exists(): if not self.can_join:
return True
if user.is_authenticated and self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return True
if not self.is_visible:
return False return False
if self.start_time is not None and self.start_time > timezone.now(): if not self.show_scoreboard and not self.is_in_contest(user):
return False return False
if self.hide_scoreboard and not self.is_in_contest(user) and self.end_time > timezone.now(): return True
def can_see_full_scoreboard(self, user):
if self.show_scoreboard:
return True
if not user.is_authenticated:
return False
if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
return True
if user.profile.id in self.editor_ids:
return True
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return True
if self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION and self.has_completed_contest(user):
return True
return False
def has_completed_contest(self, user):
if user.is_authenticated:
participation = self.users.filter(virtual=ContestParticipation.LIVE, user=user.profile).first()
if participation and participation.ended:
return True
return False
@cached_property
def show_scoreboard(self):
if not self.can_join:
return False
if (self.scoreboard_visibility in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) and
not self.ended):
return False return False
return True return True
@ -186,6 +246,19 @@ class Contest(models.Model):
def ended(self): def ended(self):
return self.end_time < self._now return self.end_time < self._now
@cached_property
def author_ids(self):
return Contest.authors.through.objects.filter(contest=self).values_list('profile_id', flat=True)
@cached_property
def editor_ids(self):
return self.author_ids.union(
Contest.curators.through.objects.filter(contest=self).values_list('profile_id', flat=True))
@cached_property
def tester_ids(self):
return Contest.testers.through.objects.filter(contest=self).values_list('profile_id', flat=True)
def __str__(self): def __str__(self):
return self.name return self.name
@ -198,50 +271,111 @@ class Contest(models.Model):
update_user_count.alters_data = True update_user_count.alters_data = True
@cached_property class Inaccessible(Exception):
def show_scoreboard(self): pass
if self.hide_scoreboard and not self.ended:
return False class PrivateContest(Exception):
return True pass
def access_check(self, user):
# Do unauthenticated check here so we can skip authentication checks later on.
if not user.is_authenticated:
# Unauthenticated users can only see visible, non-private contests
if not self.is_visible:
raise self.Inaccessible()
if self.is_private or self.is_organization_private:
raise self.PrivateContest()
return
# If the user can view or edit all contests
if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
return
# User is organizer or curator for contest
if user.profile.id in self.editor_ids:
return
# User is tester for contest
if user.profile.id in self.tester_ids:
return
# Contest is not publicly visible
if not self.is_visible:
raise self.Inaccessible()
# Contest is not private
if not self.is_private and not self.is_organization_private:
return
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return
in_org = self.organizations.filter(id__in=user.profile.organizations.all()).exists()
in_users = self.private_contestants.filter(id=user.profile.id).exists()
if not self.is_private and self.is_organization_private:
if in_org:
return
raise self.PrivateContest()
if self.is_private and not self.is_organization_private:
if in_users:
return
raise self.PrivateContest()
if self.is_private and self.is_organization_private:
if in_org and in_users:
return
raise self.PrivateContest()
def is_accessible_by(self, user): def is_accessible_by(self, user):
# Contest is publicly visible try:
if self.is_visible: self.access_check(user)
# Contest is not private except (self.Inaccessible, self.PrivateContest):
if not self.is_private and not self.is_organization_private: return False
return True else:
if user.is_authenticated:
# User is in the organizations it is private to
if self.organizations.filter(id__in=user.profile.organizations.all()).exists():
return True
# User is in the group of private contestants
if self.private_contestants.filter(id=user.profile.id).exists():
return True
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return True
# If the user can view all contests
if user.has_perm('judge.see_private_contest'):
return True return True
# User can edit the contest
return self.is_editable_by(user)
def is_editable_by(self, user): def is_editable_by(self, user):
# If the user can edit all contests # If the user can edit all contests
if user.has_perm('judge.edit_all_contest'): if user.has_perm('judge.edit_all_contest'):
return True return True
# If the user is a contest organizer # If the user is a contest organizer or curator
if user.has_perm('judge.edit_own_contest') and \ if user.has_perm('judge.edit_own_contest') and user.profile.id in self.editor_ids:
self.organizers.filter(id=user.profile.id).exists():
return True return True
return False return False
@classmethod
def get_visible_contests(cls, user):
if not user.is_authenticated:
return cls.objects.filter(is_visible=True, is_organization_private=False, is_private=False) \
.defer('description').distinct()
queryset = cls.objects.defer('description')
if not (user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest')):
q = Q(is_visible=True)
q &= (
Q(view_contest_scoreboard=user.profile) |
Q(is_organization_private=False, is_private=False) |
Q(is_organization_private=False, is_private=True, private_contestants=user.profile) |
Q(is_organization_private=True, is_private=False, organizations__in=user.profile.organizations.all()) |
Q(is_organization_private=True, is_private=True, organizations__in=user.profile.organizations.all(),
private_contestants=user.profile)
)
q |= Q(authors=user.profile)
q |= Q(curators=user.profile)
q |= Q(testers=user.profile)
queryset = queryset.filter(q)
return queryset.distinct()
def rate(self): def rate(self):
Rating.objects.filter(contest__end_time__gte=self.end_time).delete() Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete()
for contest in Contest.objects.filter(is_rated=True, end_time__gte=self.end_time).order_by('end_time'): for contest in Contest.objects.filter(
is_rated=True, end_time__range=(self.end_time, self._now),
).order_by('end_time'):
rate_contest(contest) rate_contest(contest)
class Meta: class Meta:
@ -255,6 +389,7 @@ class Contest(models.Model):
('contest_access_code', _('Contest access codes')), ('contest_access_code', _('Contest access codes')),
('create_private_contest', _('Create private contests')), ('create_private_contest', _('Create private contests')),
('change_contest_visibility', _('Change contest visibility')), ('change_contest_visibility', _('Change contest visibility')),
('contest_problem_label', _('Edit contest problem label script')),
) )
verbose_name = _('contest') verbose_name = _('contest')
verbose_name_plural = _('contests') verbose_name_plural = _('contests')
@ -271,6 +406,7 @@ class ContestParticipation(models.Model):
cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0) cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0)
is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False, is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False,
help_text=_('Whether this participation is disqualified.')) help_text=_('Whether this participation is disqualified.'))
tiebreaker = models.FloatField(verbose_name=_('tie-breaking field'), default=0.0)
virtual = models.IntegerField(verbose_name=_('virtual participation id'), default=LIVE, virtual = models.IntegerField(verbose_name=_('virtual participation id'), default=LIVE,
help_text=_('0 means non-virtual, otherwise the n-th virtual participation.')) help_text=_('0 means non-virtual, otherwise the n-th virtual participation.'))
format_data = JSONField(verbose_name=_('contest format specific data'), null=True, blank=True) format_data = JSONField(verbose_name=_('contest format specific data'), null=True, blank=True)
@ -395,7 +531,8 @@ class Rating(models.Model):
related_name='rating', on_delete=CASCADE) related_name='rating', on_delete=CASCADE)
rank = models.IntegerField(verbose_name=_('rank')) rank = models.IntegerField(verbose_name=_('rank'))
rating = models.IntegerField(verbose_name=_('rating')) rating = models.IntegerField(verbose_name=_('rating'))
volatility = models.IntegerField(verbose_name=_('volatility')) mean = models.FloatField(verbose_name=_('raw rating'))
performance = models.FloatField(verbose_name=_('contest performance'))
last_rated = models.DateTimeField(db_index=True, verbose_name=_('last rated')) last_rated = models.DateTimeField(db_index=True, verbose_name=_('last rated'))
class Meta: class Meta:

View file

@ -5,7 +5,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.cache import cache from django.core.cache import cache
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models from django.db import models
from django.db.models import CASCADE, F, QuerySet, SET_NULL from django.db.models import CASCADE, F, Q, QuerySet, SET_NULL
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.urls import reverse from django.urls import reverse
@ -219,6 +219,43 @@ class Problem(models.Model):
def is_subs_manageable_by(self, user): def is_subs_manageable_by(self, user):
return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user) return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user)
@classmethod
def get_visible_problems(cls, user):
# Do unauthenticated check here so we can skip authentication checks later on.
if not user.is_authenticated:
return cls.get_public_problems()
# Conditions for visible problem:
# - `judge.edit_all_problem` or `judge.see_private_problem`
# - otherwise
# - not is_public problems
# - author or curator or tester
# - is_public problems
# - not is_organization_private or in organization or `judge.see_organization_problem`
# - author or curator or tester
queryset = cls.objects.defer('description')
if not (user.has_perm('judge.see_private_problem') or user.has_perm('judge.edit_all_problem')):
q = Q(is_public=True)
if not user.has_perm('judge.see_organization_problem'):
# Either not organization private or in the organization.
q &= (
Q(is_organization_private=False) |
Q(is_organization_private=True, organizations__in=user.profile.organizations.all())
)
# Authors, curators, and testers should always have access, so OR at the very end.
q |= Q(authors=user.profile)
q |= Q(curators=user.profile)
q |= Q(testers=user.profile)
queryset = queryset.filter(q)
return queryset
@classmethod
def get_public_problems(cls):
return cls.objects.filter(is_public=True, is_organization_private=False).defer('description')
def __str__(self): def __str__(self):
return self.name return self.name

View file

@ -1,11 +1,13 @@
import errno import errno
import os import os
from zipfile import BadZipFile, ZipFile
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from judge.utils.problem_data import ProblemDataStorage from judge.utils.problem_data import ProblemDataStorage, get_file_cachekey
__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS'] __all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS']
@ -66,7 +68,16 @@ class ProblemData(models.Model):
self.__original_zipfile = self.zipfile self.__original_zipfile = self.zipfile
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.zipfile != self.__original_zipfile: if self.zipfile != self.__original_zipfile and self.__original_zipfile:
# Delete caches
try:
files = ZipFile(self.__original_zipfile.path).namelist()
for file in files:
cache_key = 'problem_archive:%s:%s' % (self.problem.code, get_file_cachekey(file))
cache.delete(cache_key)
except BadZipFile:
pass
self.__original_zipfile.delete(save=False) self.__original_zipfile.delete(save=False)
return super(ProblemData, self).save(*args, **kwargs) return super(ProblemData, self).save(*args, **kwargs)

View file

@ -136,12 +136,15 @@ class Profile(models.Model):
def calculate_points(self, table=_pp_table): def calculate_points(self, table=_pp_table):
from judge.models import Problem from judge.models import Problem
data = (Problem.objects.filter(submission__user=self, submission__points__isnull=False, is_public=True, public_problems = Problem.get_public_problems()
is_organization_private=False) data = (
.annotate(max_points=Max('submission__points')).order_by('-max_points') public_problems.filter(submission__user=self, submission__points__isnull=False)
.values_list('max_points', flat=True).filter(max_points__gt=0)) .annotate(max_points=Max('submission__points')).order_by('-max_points')
extradata = Problem.objects.filter(submission__user=self, submission__result='AC', is_public=True) \ .values_list('max_points', flat=True).filter(max_points__gt=0)
.values('id').distinct().count() )
extradata = (
public_problems.filter(submission__user=self, submission__result='AC').values('id').distinct().count()
)
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
points = sum(data) points = sum(data)
problems = len(data) problems = len(data)
@ -163,8 +166,12 @@ class Profile(models.Model):
remove_contest.alters_data = True remove_contest.alters_data = True
def update_contest(self): def update_contest(self):
contest = self.current_contest from judge.models import ContestParticipation
if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)): try:
contest = self.current_contest
if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)):
self.remove_contest()
except ContestParticipation.DoesNotExist:
self.remove_contest() self.remove_contest()
update_contest.alters_data = True update_contest.alters_data = True
@ -254,5 +261,13 @@ class Friend(models.Model):
else: else:
self.make_friend(current_user, new_friend) self.make_friend(current_user, new_friend)
@classmethod
def get_friend_profiles(self, current_user):
try:
ret = self.objects.get(current_user=current_user).users.all()
except Friend.DoesNotExist:
ret = Profile.objects.none()
return ret
def __str__(self): def __str__(self):
return str(self.current_user) return str(self.current_user)

View file

@ -97,7 +97,10 @@ class Language(models.Model):
@classmethod @classmethod
def get_default_language(cls): def get_default_language(cls):
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE) try:
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
except Language.DoesNotExist:
return cls.get_python3()
@classmethod @classmethod
def get_default_language_pk(cls): def get_default_language_pk(cls):

View file

@ -1,3 +1,4 @@
import base64
import errno import errno
import io import io
import json import json
@ -10,6 +11,20 @@ import uuid
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext from django.utils.translation import gettext
logger = logging.getLogger('judge.problem.pdf')
HAS_SELENIUM = False
if settings.USE_SELENIUM:
try:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
HAS_SELENIUM = True
except ImportError:
logger.warning('Failed to import Selenium', exc_info=True)
HAS_PHANTOMJS = os.access(settings.PHANTOMJS, os.X_OK) HAS_PHANTOMJS = os.access(settings.PHANTOMJS, os.X_OK)
HAS_SLIMERJS = os.access(settings.SLIMERJS, os.X_OK) HAS_SLIMERJS = os.access(settings.SLIMERJS, os.X_OK)
@ -18,13 +33,11 @@ PUPPETEER_MODULE = settings.PUPPETEER_MODULE
HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE) HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE)
HAS_PDF = (os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and HAS_PDF = (os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and
(HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER)) (HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER or HAS_SELENIUM))
EXIFTOOL = settings.EXIFTOOL EXIFTOOL = settings.EXIFTOOL
HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK) HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK)
logger = logging.getLogger('judge.problem.pdf')
class BasePdfMaker(object): class BasePdfMaker(object):
math_engine = 'jax' math_engine = 'jax'
@ -240,8 +253,8 @@ puppeteer.launch().then(browser => Promise.resolve()
def get_render_script(self): def get_render_script(self):
return self.template.replace('{params}', json.dumps({ return self.template.replace('{params}', json.dumps({
'input': 'file://' + os.path.abspath(os.path.join(self.dir, 'input.html')), 'input': 'file://%s' % self.htmlfile,
'output': os.path.abspath(os.path.join(self.dir, 'output.pdf')), 'output': self.pdffile,
'paper': settings.PUPPETEER_PAPER_SIZE, 'paper': settings.PUPPETEER_PAPER_SIZE,
'footer': gettext('Page [page] of [topage]'), 'footer': gettext('Page [page] of [topage]'),
})) }))
@ -257,9 +270,55 @@ puppeteer.launch().then(browser => Promise.resolve()
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env) self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env)
self.log = self.proc.communicate()[0] self.log = self.proc.communicate()[0]
class SeleniumPDFRender(BasePdfMaker):
success = False
template = {
'printBackground': True,
'displayHeaderFooter': True,
'headerTemplate': '<div></div>',
'footerTemplate': '<center style="margin: 0 auto; font-family: Segoe UI; font-size: 10px">' +
gettext('Page %s of %s') %
('<span class="pageNumber"></span>', '<span class="totalPages"></span>') +
'</center>',
}
def get_log(self, driver):
return '\n'.join(map(str, driver.get_log('driver') + driver.get_log('browser')))
def _make(self, debug):
options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--no-sandbox") # for root
options.binary_location = settings.SELENIUM_CUSTOM_CHROME_PATH
browser = webdriver.Chrome(settings.SELENIUM_CHROMEDRIVER_PATH, options=options)
browser.get('file://%s' % self.htmlfile)
self.log = self.get_log(browser)
try:
WebDriverWait(browser, 15).until(EC.presence_of_element_located((By.CLASS_NAME, 'math-loaded')))
except TimeoutException:
logger.error('PDF math rendering timed out')
self.log = self.get_log(browser) + '\nPDF math rendering timed out'
browser.quit()
return
response = browser.execute_cdp_cmd('Page.printToPDF', self.template)
self.log = self.get_log(browser)
if not response:
browser.quit()
return
with open(self.pdffile, 'wb') as f:
f.write(base64.b64decode(response['data']))
self.success = True
browser.quit()
if HAS_PUPPETEER: if HAS_PUPPETEER:
DefaultPdfMaker = PuppeteerPDFRender DefaultPdfMaker = PuppeteerPDFRender
elif HAS_SELENIUM:
DefaultPdfMaker = SeleniumPDFRender
elif HAS_SLIMERJS: elif HAS_SLIMERJS:
DefaultPdfMaker = SlimerJSPdfMaker DefaultPdfMaker = SlimerJSPdfMaker
elif HAS_PHANTOMJS: elif HAS_PHANTOMJS:

View file

@ -1,162 +1,197 @@
import math
from bisect import bisect from bisect import bisect
from operator import itemgetter from math import pi, sqrt, tanh
from operator import attrgetter, itemgetter
from django.db import connection, transaction from django.db import transaction
from django.db.models import Count from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from judge.utils.ranker import tie_ranker
BETA2 = 328.33 ** 2
RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
MEAN_INIT = 1400.
VAR_INIT = 250**2 * (BETA2 / 212**2)
SD_INIT = sqrt(VAR_INIT)
VALID_RANGE = MEAN_INIT - 20 * SD_INIT, MEAN_INIT + 20 * SD_INIT
VAR_PER_CONTEST = 1219.047619 * (BETA2 / 212**2)
VAR_LIM = (sqrt(VAR_PER_CONTEST**2 + 4 * BETA2 * VAR_PER_CONTEST) - VAR_PER_CONTEST) / 2
SD_LIM = sqrt(VAR_LIM)
TANH_C = sqrt(3) / pi
def rational_approximation(t): def tie_ranker(iterable, key=attrgetter('points')):
# Abramowitz and Stegun formula 26.2.23. rank = 0
# The absolute value of the error should be less than 4.5 e-4. delta = 1
c = [2.515517, 0.802853, 0.010328] last = None
d = [1.432788, 0.189269, 0.001308] buf = []
numerator = (c[2] * t + c[1]) * t + c[0] for item in iterable:
denominator = ((d[2] * t + d[1]) * t + d[0]) * t + 1.0 new = key(item)
return t - numerator / denominator if new != last:
for _ in buf:
yield rank + (delta - 1) / 2.0
rank += delta
delta = 0
buf = []
delta += 1
buf.append(item)
last = key(item)
for _ in buf:
yield rank + (delta - 1) / 2.0
def normal_CDF_inverse(p): def eval_tanhs(tanh_terms, x):
assert 0.0 < p < 1 return sum((wt / sd) * tanh((x - mu) / (2 * sd)) for mu, sd, wt in tanh_terms)
# See article above for explanation of this section.
if p < 0.5:
# F^-1(p) = - G^-1(p)
return -rational_approximation(math.sqrt(-2.0 * math.log(p)))
else:
# F^-1(p) = G^-1(1-p)
return rational_approximation(math.sqrt(-2.0 * math.log(1.0 - p)))
def WP(RA, RB, VA, VB): def solve(tanh_terms, y_tg, lin_factor=0, bounds=VALID_RANGE):
return (math.erf((RB - RA) / math.sqrt(2 * (VA * VA + VB * VB))) + 1) / 2.0 L, R = bounds
Ly, Ry = None, None
while R - L > 2:
def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated): x = (L + R) / 2
# actual_rank: 1 is first place, N is last place y = lin_factor * x + eval_tanhs(tanh_terms, x)
# if there are ties, use the average of places (if places 2, 3, 4, 5 tie, use 3.5 for all of them) if y > y_tg:
R, Ry = x, y
N = len(old_rating) elif y < y_tg:
new_rating = old_rating[:] L, Ly = x, y
new_volatility = old_volatility[:]
if N <= 1:
return new_rating, new_volatility
ranking = list(range(N))
ranking.sort(key=old_rating.__getitem__, reverse=True)
ave_rating = float(sum(old_rating)) / N
sum1 = sum(i * i for i in old_volatility) / N
sum2 = sum((i - ave_rating) ** 2 for i in old_rating) / (N - 1)
CF = math.sqrt(sum1 + sum2)
for i in range(N):
ERank = 0.5
for j in range(N):
ERank += WP(old_rating[i], old_rating[j], old_volatility[i], old_volatility[j])
EPerf = -normal_CDF_inverse((ERank - 0.5) / N)
APerf = -normal_CDF_inverse((actual_rank[i] - 0.5) / N)
PerfAs = old_rating[i] + CF * (APerf - EPerf)
Weight = 1.0 / (1 - (0.42 / (times_rated[i] + 1) + 0.18)) - 1.0
if old_rating[i] > 2500:
Weight *= 0.8
elif old_rating[i] >= 2000:
Weight *= 0.9
Cap = 150.0 + 1500.0 / (times_rated[i] + 2)
new_rating[i] = (old_rating[i] + Weight * PerfAs) / (1.0 + Weight)
if times_rated[i] == 0:
new_volatility[i] = 385
else: else:
new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight + return x
(old_volatility[i] ** 2) / (Weight + 1)) # Use linear interpolation to be slightly more accurate.
if abs(old_rating[i] - new_rating[i]) > Cap: if Ly is None:
if old_rating[i] < new_rating[i]: Ly = lin_factor * L + eval_tanhs(tanh_terms, L)
new_rating[i] = old_rating[i] + Cap if y_tg <= Ly:
else: return L
new_rating[i] = old_rating[i] - Cap if Ry is None:
Ry = lin_factor * R + eval_tanhs(tanh_terms, R)
if y_tg >= Ry:
return R
ratio = (y_tg - Ly) / (Ry - Ly)
return L * (1 - ratio) + R * ratio
# try to keep the sum of ratings constant
adjust = float(sum(old_rating) - sum(new_rating)) / N def get_var(times_ranked, cache=[VAR_INIT]):
new_rating = list(map(adjust.__add__, new_rating)) while times_ranked >= len(cache):
# inflate a little if we have to so people who placed first don't lose rating next_var = 1. / (1. / (cache[-1] + VAR_PER_CONTEST) + 1. / BETA2)
best_rank = min(actual_rank) cache.append(next_var)
for i in range(N): return cache[times_ranked]
if abs(actual_rank[i] - best_rank) <= 1e-3 and new_rating[i] < old_rating[i] + 1:
new_rating[i] = old_rating[i] + 1
return list(map(int, map(round, new_rating))), list(map(int, map(round, new_volatility))) def recalculate_ratings(ranking, old_mean, times_ranked, historical_p):
n = len(ranking)
new_p = [0.] * n
new_mean = [0.] * n
# Note: pre-multiply delta by TANH_C to improve efficiency.
delta = [TANH_C * sqrt(get_var(t) + VAR_PER_CONTEST + BETA2) for t in times_ranked]
p_tanh_terms = [(m, d, 1) for m, d in zip(old_mean, delta)]
# Calculate performance at index i.
def solve_idx(i, bounds=VALID_RANGE):
r = ranking[i]
y_tg = 0
for d, s in zip(delta, ranking):
if s > r: # s loses to r
y_tg += 1. / d
elif s < r: # s beats r
y_tg -= 1. / d
# Otherwise, this is a tie that counts as half a win, as per Elo-MMR.
new_p[i] = solve(p_tanh_terms, y_tg, bounds=bounds)
# Fill all indices between i and j, inclusive. Use the fact that new_p is non-increasing.
def divconq(i, j):
if j - i > 1:
k = (i + j) // 2
solve_idx(k, bounds=(new_p[j], new_p[i]))
divconq(i, k)
divconq(k, j)
if n < 2:
new_p = list(old_mean)
new_mean = list(old_mean)
else:
# Calculate performance.
solve_idx(0)
solve_idx(n - 1)
divconq(0, n - 1)
# Calculate mean.
for i, r in enumerate(ranking):
tanh_terms = []
w_prev = 1.
w_sum = 0.
for j, h in enumerate([new_p[i]] + historical_p[i]):
gamma2 = (VAR_PER_CONTEST if j > 0 else 0)
h_var = get_var(times_ranked[i] + 1 - j)
k = h_var / (h_var + gamma2)
w = w_prev * k**2
# Future optimization: If j is around 20, then w < 1e-3 and it is possible to break early.
tanh_terms.append((h, sqrt(BETA2) * TANH_C, w))
w_prev = w
w_sum += w / BETA2
w0 = 1. / get_var(times_ranked[i] + 1) - w_sum
p0 = eval_tanhs(tanh_terms[1:], old_mean[i]) / w0 + old_mean[i]
new_mean[i] = solve(tanh_terms, w0 * p0, lin_factor=w0)
# Display a slightly lower rating to incentivize participation.
# As times_ranked increases, new_rating converges to new_mean.
new_rating = [max(1, round(m - (sqrt(get_var(t + 1)) - SD_LIM))) for m, t in zip(new_mean, times_ranked)]
return new_rating, new_mean, new_p
def rate_contest(contest): def rate_contest(contest):
from judge.models import Rating, Profile from judge.models import Rating, Profile
cursor = connection.cursor() rating_subquery = Rating.objects.filter(user=OuterRef('user'))
cursor.execute(''' rating_sorted = rating_subquery.order_by('-contest__end_time')
SELECT judge_rating.user_id, judge_rating.rating, judge_rating.volatility, r.times users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \
FROM judge_rating INNER JOIN .annotate(submissions=Count('submission'),
judge_contest ON (judge_contest.id = judge_rating.contest_id) INNER JOIN ( last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), RATING_INIT),
SELECT judge_rating.user_id AS id, MAX(judge_contest.end_time) AS last_time, last_mean=Coalesce(Subquery(rating_sorted.values('mean')[:1]), MEAN_INIT),
COUNT(judge_rating.user_id) AS times times=Coalesce(Subquery(rating_subquery.order_by().values('user_id')
FROM judge_contestparticipation INNER JOIN .annotate(count=Count('id')).values('count')), 0)) \
judge_rating ON (judge_rating.user_id = judge_contestparticipation.user_id) INNER JOIN .exclude(user_id__in=contest.rate_exclude.all()) \
judge_contest ON (judge_contest.id = judge_rating.contest_id) .filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker',
WHERE judge_contestparticipation.contest_id = %s AND judge_contest.end_time < %s AND 'last_rating', 'last_mean', 'times')
judge_contestparticipation.user_id NOT IN (
SELECT profile_id FROM judge_contest_rate_exclude WHERE contest_id = %s
) AND judge_contestparticipation.virtual = 0
GROUP BY judge_rating.user_id
ORDER BY judge_contestparticipation.score DESC, judge_contestparticipation.cumtime ASC
) AS r ON (judge_rating.user_id = r.id AND judge_contest.end_time = r.last_time)
''', (contest.id, contest.end_time, contest.id))
data = {user: (rating, volatility, times) for user, rating, volatility, times in cursor.fetchall()}
cursor.close()
users = contest.users.order_by('is_disqualified', '-score', 'cumtime').annotate(submissions=Count('submission')) \
.exclude(user_id__in=contest.rate_exclude.all()).filter(virtual=0, user__is_unlisted=False) \
.values_list('id', 'user_id', 'score', 'cumtime')
if not contest.rate_all: if not contest.rate_all:
users = users.filter(submissions__gt=0) users = users.filter(submissions__gt=0)
if contest.rating_floor is not None: if contest.rating_floor is not None:
users = users.exclude(user__rating__lt=contest.rating_floor) users = users.exclude(last_rating__lt=contest.rating_floor)
if contest.rating_ceiling is not None: if contest.rating_ceiling is not None:
users = users.exclude(user__rating__gt=contest.rating_ceiling) users = users.exclude(last_rating__gt=contest.rating_ceiling)
users = list(tie_ranker(users, key=itemgetter(2, 3)))
participation_ids = [user[1][0] for user in users] users = list(users)
user_ids = [user[1][1] for user in users] participation_ids = list(map(itemgetter('id'), users))
ranking = list(map(itemgetter(0), users)) user_ids = list(map(itemgetter('user_id'), users))
old_data = [data.get(user, (1200, 535, 0)) for user in user_ids] ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
old_rating = list(map(itemgetter(0), old_data)) old_mean = list(map(itemgetter('last_mean'), users))
old_volatility = list(map(itemgetter(1), old_data)) times_ranked = list(map(itemgetter('times'), users))
times_ranked = list(map(itemgetter(2), old_data)) historical_p = [[] for _ in users]
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked)
user_id_to_idx = {uid: i for i, uid in enumerate(user_ids)}
for h in Rating.objects.filter(user_id__in=user_ids) \
.order_by('-contest__end_time') \
.values('user_id', 'performance'):
idx = user_id_to_idx[h['user_id']]
historical_p[idx].append(h['performance'])
rating, mean, performance = recalculate_ratings(ranking, old_mean, times_ranked, historical_p)
now = timezone.now() now = timezone.now()
ratings = [Rating(user_id=id, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z) ratings = [Rating(user_id=i, contest=contest, rating=r, mean=m, performance=perf,
for id, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)] last_rated=now, participation_id=pid, rank=z)
cursor = connection.cursor() for i, pid, r, m, perf, z in zip(user_ids, participation_ids, rating, mean, performance, ranking)]
cursor.execute('CREATE TEMPORARY TABLE _profile_rating_update(id integer, rating integer)')
cursor.executemany('INSERT INTO _profile_rating_update VALUES (%s, %s)', list(zip(user_ids, rating)))
with transaction.atomic(): with transaction.atomic():
Rating.objects.filter(contest=contest).delete()
Rating.objects.bulk_create(ratings) Rating.objects.bulk_create(ratings)
cursor.execute('''
UPDATE `%s` p INNER JOIN `_profile_rating_update` tmp ON (p.id = tmp.id) Profile.objects.filter(contest_history__contest=contest, contest_history__virtual=0).update(
SET p.rating = tmp.rating rating=Subquery(Rating.objects.filter(user=OuterRef('id'))
''' % Profile._meta.db_table) .order_by('-contest__end_time').values('rating')[:1]))
cursor.execute('DROP TABLE _profile_rating_update')
cursor.close()
return old_rating, old_volatility, ranking, times_ranked, rating, volatility
RATING_LEVELS = ['Newbie', 'Amateur', 'Expert', 'Candidate Master', 'Master', 'Grandmaster', 'Target'] RATING_LEVELS = ['Newbie', 'Amateur', 'Expert', 'Candidate Master', 'Master', 'Grandmaster', 'Target']
RATING_VALUES = [1000, 1200, 1500, 1800, 2200, 3000] RATING_VALUES = [1000, 1400, 1700, 1900, 2100, 2400, 3000]
RATING_CLASS = ['rate-newbie', 'rate-amateur', 'rate-expert', 'rate-candidate-master', RATING_CLASS = ['rate-newbie', 'rate-amateur', 'rate-specialist', 'rate-expert', 'rate-candidate-master',
'rate-master', 'rate-grandmaster', 'rate-target'] 'rate-master', 'rate-grandmaster', 'rate-target']
@ -178,4 +213,4 @@ def rating_progress(rating):
return 1.0 return 1.0
prev = 0 if not level else RATING_VALUES[level - 1] prev = 0 if not level else RATING_VALUES[level - 1]
next = RATING_VALUES[level] next = RATING_VALUES[level]
return (rating - prev + 0.0) / (next - prev) return (rating - prev + 0.0) / (next - prev)

View file

@ -11,7 +11,7 @@ class ProblemSitemap(Sitemap):
priority = 0.8 priority = 0.8
def items(self): def items(self):
return Problem.objects.filter(is_public=True, is_organization_private=False).values_list('code') return Problem.get_public_problems().values_list('code')
def location(self, obj): def location(self, obj):
return reverse('problem_detail', args=obj) return reverse('problem_detail', args=obj)

View file

@ -83,7 +83,7 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs):
if is_new: if is_new:
if not hasattr(user, 'profile'): if not hasattr(user, 'profile'):
profile = Profile(user=user) profile = Profile(user=user)
profile.language = Language.get_python3() profile.language = Language.get_default_language()
logger.info('Info from %s: %s', backend.name, response) logger.info('Info from %s: %s', backend.name, response)
profile.save() profile.save()
form = ProfileForm(instance=profile, user=user) form = ProfileForm(instance=profile, user=user)

View file

@ -1,3 +1,4 @@
from judge.tasks.contest import *
from judge.tasks.demo import * from judge.tasks.demo import *
from judge.tasks.contest import * from judge.tasks.contest import *
from judge.tasks.submission import * from judge.tasks.submission import *

View file

@ -1,10 +1,6 @@
from django.contrib.auth.models import User from judge.models import SubmissionTestCase, Problem
from django.conf import settings
from judge.models import SubmissionTestCase, Problem, Profile, Language, Organization
from collections import defaultdict from collections import defaultdict
import csv
def generate_report(problem): def generate_report(problem):
testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all() testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all()
@ -21,52 +17,4 @@ def generate_report(problem):
rate[i] = score[i] / total[i] rate[i] = score[i] / total[i]
for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True): for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True):
print(i, score[i], total[i], rate[i]) print(i, score[i], total[i], rate[i])
def import_users(csv_file):
# 1st row: username, password, name, organization
# ... row: a_username, passhere, my_name, organ
try:
f = open(csv_file, 'r')
except OSError:
print("Could not open csv file", csv_file)
return
with f:
reader = csv.DictReader(f)
for row in reader:
try:
username = row['username']
pwd = row['password']
except Exception:
print('username and/or password column missing')
print('Make sure your columns are: username, password, name, organization')
user, created = User.objects.get_or_create(username=username, defaults={
'is_active': True,
})
profile, _ = Profile.objects.get_or_create(user=user, defaults={
'language': Language.get_python3(),
'timezone': settings.DEFAULT_USER_TIME_ZONE,
})
if created:
print('Created user', username)
if pwd:
user.set_password(pwd)
elif created:
user.set_password('lqdoj')
print('User', username, 'missing password, default=lqdoj')
if 'name' in row.keys() and row['name']:
user.first_name = row['name']
if 'organization' in row.keys() and row['organization']:
org = Organization.objects.get(name=row['organization'])
profile.organizations.add(org)
user.save()
profile.save()

100
judge/tasks/import_users.py Normal file
View file

@ -0,0 +1,100 @@
import csv
from tempfile import mktemp
from django.conf import settings
from django.contrib.auth.models import User
from judge.models import Profile, Language, Organization
fields = ['username', 'password', 'name', 'school', 'email', 'organizations']
descriptions = ['my_username(edit old one if exist)',
'123456 (must have)',
'Le Van A (can be empty)',
'Le Quy Don (can be empty)',
'email@email.com (can be empty)',
'org1&org2&org3&... (can be empty - org slug in URL)']
def csv_to_dict(csv_file):
rows = csv.reader(csv_file.read().decode().split('\n'))
header = next(rows)
header = [i.lower() for i in header]
if 'username' not in header:
return []
res = []
for row in rows:
if len(row) != len(header):
continue
cur_dict = {i: '' for i in fields}
for i in range(len(header)):
if header[i] not in fields:
continue
cur_dict[header[i]] = row[i]
if cur_dict['username']:
res.append(cur_dict)
return res
# return result log
def import_users(users):
log = ''
for i, row in enumerate(users):
cur_log = str(i + 1) + '. '
username = row['username']
cur_log += username + ': '
pwd = row['password']
user, created = User.objects.get_or_create(username=username, defaults={
'is_active': True,
})
profile, _ = Profile.objects.get_or_create(user=user, defaults={
'language': Language.get_python3(),
'timezone': settings.DEFAULT_USER_TIME_ZONE,
})
if created:
cur_log += 'Create new - '
else:
cur_log += 'Edit - '
if pwd:
user.set_password(pwd)
elif created:
user.set_password('lqdoj')
cur_log += 'Missing password, set password = lqdoj - '
if 'name' in row.keys() and row['name']:
user.first_name = row['name']
if 'school' in row.keys() and row['school']:
user.last_name = row['school']
if row['organizations']:
orgs = row['organizations'].split('&')
added_orgs = []
for o in orgs:
try:
org = Organization.objects.get(slug=o)
profile.organizations.add(org)
added_orgs.append(org.name)
except Organization.DoesNotExist:
continue
if added_orgs:
cur_log += 'Added to ' + ', '.join(added_orgs) + ' - '
if row['email']:
user.email = row['email']
user.save()
profile.save()
cur_log += 'Saved\n'
log += cur_log
log += 'FINISH'
return log

View file

@ -0,0 +1,87 @@
# https://github.com/FineUploader/server-examples/blob/master/python/django-fine-uploader
from django.conf import settings
from django import forms
from django.forms import ClearableFileInput
import os, os.path
import tempfile
import shutil
__all__ = (
'handle_upload', 'save_upload', 'FineUploadForm', 'FineUploadFileInput'
)
def combine_chunks(total_parts, total_size, source_folder, dest):
if not os.path.exists(os.path.dirname(dest)):
os.makedirs(os.path.dirname(dest))
with open(dest, 'wb+') as destination:
for i in range(total_parts):
part = os.path.join(source_folder, str(i))
with open(part, 'rb') as source:
destination.write(source.read())
def save_upload(f, path):
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
with open(path, 'wb+') as destination:
if hasattr(f, 'multiple_chunks') and f.multiple_chunks():
for chunk in f.chunks():
destination.write(chunk)
else:
destination.write(f.read())
# pass callback function to post_upload
def handle_upload(f, fileattrs, upload_dir, post_upload=None):
chunks_dir = os.path.join(tempfile.gettempdir(), 'chunk_upload_tmp')
if not os.path.exists(os.path.dirname(chunks_dir)):
os.makedirs(os.path.dirname(chunks_dir))
chunked = False
dest_folder = upload_dir
dest = os.path.join(dest_folder, fileattrs['qqfilename'])
# Chunked
if fileattrs.get('qqtotalparts') and int(fileattrs['qqtotalparts']) > 1:
chunked = True
dest_folder = os.path.join(chunks_dir, fileattrs['qquuid'])
dest = os.path.join(dest_folder, fileattrs['qqfilename'], str(fileattrs['qqpartindex']))
save_upload(f, dest)
# If the last chunk has been sent, combine the parts.
if chunked and (fileattrs['qqtotalparts'] - 1 == fileattrs['qqpartindex']):
combine_chunks(fileattrs['qqtotalparts'],
fileattrs['qqtotalfilesize'],
source_folder=os.path.dirname(dest),
dest=os.path.join(upload_dir, fileattrs['qqfilename']))
shutil.rmtree(os.path.dirname(os.path.dirname(dest)))
if post_upload and (not chunked or fileattrs['qqtotalparts'] - 1 == fileattrs['qqpartindex']):
post_upload()
class FineUploadForm(forms.Form):
qqfile = forms.FileField()
qquuid = forms.CharField()
qqfilename = forms.CharField()
qqpartindex = forms.IntegerField(required=False)
qqchunksize = forms.IntegerField(required=False)
qqpartbyteoffset = forms.IntegerField(required=False)
qqtotalfilesize = forms.IntegerField(required=False)
qqtotalparts = forms.IntegerField(required=False)
class FineUploadFileInput(ClearableFileInput):
template_name = 'widgets/fine_uploader.html'
def fine_uploader_id(self, name):
return name + '_' + 'fine_uploader'
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget'].update({
'fine_uploader_id': self.fine_uploader_id(name),
})
return context

View file

@ -1,15 +1,17 @@
import hashlib
import json import json
import os import os
import re import re
import shutil import shutil
import yaml import yaml
import zipfile
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.core.cache import cache
VALIDATOR_TEMPLATE_PATH = 'validator_template/template.py' VALIDATOR_TEMPLATE_PATH = 'validator_template/template.py'
@ -232,3 +234,61 @@ class ProblemDataCompiler(object):
def generate(cls, *args, **kwargs): def generate(cls, *args, **kwargs):
self = cls(*args, **kwargs) self = cls(*args, **kwargs)
self.compile() self.compile()
def get_visible_content(data):
data = data or b''
data = data.replace(b'\r\n', b'\r').replace(b'\r', b'\n')
data = data.decode('utf-8')
if (len(data) > settings.TESTCASE_VISIBLE_LENGTH):
data = data[:settings.TESTCASE_VISIBLE_LENGTH]
data += '.' * 3
return data
def get_file_cachekey(file):
return hashlib.sha1(file.encode()).hexdigest()
def get_problem_case(problem, files):
result = {}
uncached_files = []
for file in files:
cache_key = 'problem_archive:%s:%s' % (problem.code, get_file_cachekey(file))
qs = cache.get(cache_key)
if qs is None:
uncached_files.append(file)
else:
result[file] = qs
if not uncached_files:
return result
archive_path = os.path.join(settings.DMOJ_PROBLEM_DATA_ROOT,
str(problem.data_files.zipfile))
if not os.path.exists(archive_path):
raise Exception(
'archive file "%s" does not exist' % archive_path)
try:
archive = zipfile.ZipFile(archive_path, 'r')
except zipfile.BadZipfile:
raise Exception('bad archive: "%s"' % archive_path)
for file in uncached_files:
cache_key = 'problem_archive:%s:%s' % (problem.code, get_file_cachekey(file))
with archive.open(file) as f:
s = f.read(settings.TESTCASE_VISIBLE_LENGTH + 3)
# add this so there are no characters left behind (ex, 'á' = 2 utf-8 chars)
while True:
try:
s.decode('utf-8')
break
except UnicodeDecodeError:
s += f.read(1)
qs = get_visible_content(s)
cache.set(cache_key, qs, 86400)
result[file] = qs
return result

View file

@ -1,6 +1,8 @@
from collections import defaultdict from collections import defaultdict
from math import e from math import e
import os, zipfile
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When
from django.db.models.fields import FloatField from django.db.models.fields import FloatField
@ -112,8 +114,8 @@ def hot_problems(duration, limit):
cache_key = 'hot_problems:%d:%d' % (duration.total_seconds(), limit) cache_key = 'hot_problems:%d:%d' % (duration.total_seconds(), limit)
qs = cache.get(cache_key) qs = cache.get(cache_key)
if qs is None: if qs is None:
qs = Problem.objects.filter(is_public=True, is_organization_private=False, qs = Problem.get_public_problems() \
submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25) .filter(submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25)
qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True) qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True)
if not qs0: if not qs0:
@ -144,4 +146,4 @@ def hot_problems(duration, limit):
)).order_by('-ordering').defer('description')[:limit] )).order_by('-ordering').defer('description')[:limit]
cache.set(cache_key, qs, 900) cache.set(cache_key, qs, 900)
return qs return qs

View file

@ -13,22 +13,3 @@ def ranker(iterable, key=attrgetter('points'), rank=0):
yield rank, item yield rank, item
last = key(item) last = key(item)
def tie_ranker(iterable, key=attrgetter('points')):
rank = 0
delta = 1
last = None
buf = []
for item in iterable:
new = key(item)
if new != last:
for i in buf:
yield rank + (delta - 1) / 2.0, i
rank += delta
delta = 0
buf = []
delta += 1
buf.append(item)
last = key(item)
for i in buf:
yield rank + (delta - 1) / 2.0, i

View file

@ -51,3 +51,18 @@ def get_bar_chart(data, **kwargs):
}, },
], ],
} }
def get_histogram(data, **kwargs):
return {
'labels': [round(i, 1) for i in list(map(itemgetter(0), data))],
'datasets': [
{
'backgroundColor': kwargs.get('fillColor', 'rgba(151,187,205,0.5)'),
'borderColor': kwargs.get('strokeColor', 'rgba(151,187,205,0.8)'),
'borderWidth': 1,
'hoverBackgroundColor': kwargs.get('highlightFill', 'rgba(151,187,205,0.75)'),
'hoverBorderColor': kwargs.get('highlightStroke', 'rgba(151,187,205,1)'),
'data': list(map(itemgetter(1), data)),
},
],
}

View file

@ -4,7 +4,7 @@ from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
from django.utils.html import mark_safe
def class_view_decorator(function_decorator): def class_view_decorator(function_decorator):
"""Convert a function based decorator into a class based decorator usable """Convert a function based decorator into a class based decorator usable

View file

@ -16,10 +16,9 @@ def sane_time_repr(delta):
def api_v1_contest_list(request): def api_v1_contest_list(request):
queryset = Contest.objects.filter(is_visible=True, is_private=False, queryset = Contest.get_visible_contests(request.user).prefetch_related(
is_organization_private=False).prefetch_related( Prefetch('tags', queryset=ContestTag.objects.only('name'), to_attr='tag_list'))
Prefetch('tags', queryset=ContestTag.objects.only('name'), to_attr='tag_list')).defer('description')
return JsonResponse({c.key: { return JsonResponse({c.key: {
'name': c.name, 'name': c.name,
'start_time': c.start_time.isoformat(), 'start_time': c.start_time.isoformat(),
@ -33,13 +32,10 @@ def api_v1_contest_detail(request, contest):
contest = get_object_or_404(Contest, key=contest) contest = get_object_or_404(Contest, key=contest)
in_contest = contest.is_in_contest(request.user) in_contest = contest.is_in_contest(request.user)
can_see_rankings = contest.can_see_scoreboard(request.user) can_see_rankings = contest.can_see_full_scoreboard(request.user)
if contest.hide_scoreboard and in_contest:
can_see_rankings = False
problems = list(contest.contest_problems.select_related('problem') problems = list(contest.contest_problems.select_related('problem')
.defer('problem__description').order_by('order')) .defer('problem__description').order_by('order'))
participations = (contest.users.filter(virtual=0, user__is_unlisted=False) participations = (contest.users.filter(virtual=0)
.prefetch_related('user__organizations') .prefetch_related('user__organizations')
.annotate(username=F('user__user__username')) .annotate(username=F('user__user__username'))
.order_by('-score', 'cumtime') if can_see_rankings else []) .order_by('-score', 'cumtime') if can_see_rankings else [])
@ -138,20 +134,20 @@ def api_v1_user_info(request, user):
last_rating = profile.ratings.last() last_rating = profile.ratings.last()
contest_history = {} contest_history = {}
if not profile.is_unlisted: participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True,
participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True, contest__is_private=False,
contest__is_private=False, contest__is_organization_private=False)
contest__is_organization_private=False) for contest_key, rating, mean, performance in participations.values_list(
for contest_key, rating, volatility in participations.values_list('contest__key', 'rating__rating', 'contest__key', 'rating__rating', 'rating__mean', 'rating__performance',
'rating__volatility'): ):
contest_history[contest_key] = { contest_history[contest_key] = {
'rating': rating, 'rating': rating,
'volatility': volatility, 'raw_rating': mean,
} 'performance': performance,
}
resp['contests'] = { resp['contests'] = {
'current_rating': last_rating.rating if last_rating else None, 'current_rating': last_rating.rating if last_rating else None,
'volatility': last_rating.volatility if last_rating else None,
'history': contest_history, 'history': contest_history,
} }

View file

@ -89,7 +89,6 @@ def api_v2_user_info(request):
resp['contests'] = { resp['contests'] = {
"current_rating": last_rating[0].rating if last_rating else None, "current_rating": last_rating[0].rating if last_rating else None,
"volatility": last_rating[0].volatility if last_rating else None,
'history': contest_history, 'history': contest_history,
} }

View file

@ -57,7 +57,8 @@ class PostList(ListView):
clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all())
context['has_clarifications'] = clarifications.count() > 0 context['has_clarifications'] = clarifications.count() > 0
context['clarifications'] = clarifications.order_by('-date') context['clarifications'] = clarifications.order_by('-date')
if participation.contest.is_editable_by(self.request.user):
context['can_edit_contest'] = True
context['user_count'] = lazy(Profile.objects.count, int, int) context['user_count'] = lazy(Profile.objects.count, int, int)
context['problem_count'] = lazy(Problem.objects.filter(is_public=True).count, int, int) context['problem_count'] = lazy(Problem.objects.filter(is_public=True).count, int, int)
context['submission_count'] = lazy(Submission.objects.count, int, int) context['submission_count'] = lazy(Submission.objects.count, int, int)
@ -81,17 +82,14 @@ class PostList(ListView):
.annotate(points=Max('points'), latest=Max('date')) .annotate(points=Max('points'), latest=Max('date'))
.order_by('-latest') .order_by('-latest')
[:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT])
visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \
.order_by('start_time')
visible_contests = Contest.objects.filter(is_visible=True).order_by('start_time') context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now)
q = Q(is_private=False, is_organization_private=False) context['future_contests'] = visible_contests.filter(start_time__gt=now)
if self.request.user.is_authenticated:
q |= Q(is_organization_private=True, organizations__in=user.organizations.all())
q |= Q(is_private=True, private_contestants=user)
q |= Q(view_contest_scoreboard=user)
visible_contests = visible_contests.filter(q)
context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now).distinct()
context['future_contests'] = visible_contests.filter(start_time__gt=now).distinct()
visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
profile = self.request.profile profile = self.request.profile
context['own_open_tickets'] = (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') context['own_open_tickets'] = (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id')
@ -107,6 +105,7 @@ class PostList(ListView):
context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10] context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10]
else: else:
context['open_tickets'] = [] context['open_tickets'] = []
return context return context

View file

@ -1,4 +1,5 @@
import json import json
import math
from calendar import Calendar, SUNDAY from calendar import Calendar, SUNDAY
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
@ -12,15 +13,15 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Case, Count, FloatField, IntegerField, Max, Min, Q, Sum, Value, When from django.db.models import Case, Count, F, FloatField, IntegerField, Max, Min, Q, Sum, Value, When
from django.db.models.expressions import CombinedExpression from django.db.models.expressions import CombinedExpression
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import date as date_filter
from django.urls import reverse from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import format_html from django.utils.html import format_html, escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy from django.utils.translation import gettext as _, gettext_lazy
@ -31,19 +32,21 @@ from judge import event_poster as event
from judge.comments import CommentedDetailView from judge.comments import CommentedDetailView
from judge.forms import ContestCloneForm from judge.forms import ContestCloneForm
from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \ from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
Organization, Problem, Profile, Submission Organization, Problem, Profile, Submission, ProblemClarification
from judge.tasks import run_moss from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status from judge.utils.celery import redirect_to_task_status
from judge.utils.opengraph import generate_opengraph from judge.utils.opengraph import generate_opengraph
from judge.utils.problems import _get_result_data from judge.utils.problems import _get_result_data
from judge.utils.ranker import ranker from judge.utils.ranker import ranker
from judge.utils.stats import get_bar_chart, get_pie_chart from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram
from judge.utils.views import DiggPaginatorMixin, SingleObjectFormView, TitleMixin, generic_message from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \
generic_message
from judge.widgets import HeavyPreviewPageDownWidget
__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar', __all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax', 'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list', 'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
'base_contest_ranking_list'] 'base_contest_ranking_list', 'ContestClarificationView']
def _find_contest(request, key, private_check=True): def _find_contest(request, key, private_check=True):
@ -59,29 +62,18 @@ def _find_contest(request, key, private_check=True):
class ContestListMixin(object): class ContestListMixin(object):
def get_queryset(self): def get_queryset(self):
queryset = Contest.objects.all() return Contest.get_visible_contests(self.request.user)
if not self.request.user.has_perm('judge.see_private_contest'):
q = Q(is_visible=True)
if self.request.user.is_authenticated:
q |= Q(organizers=self.request.profile)
queryset = queryset.filter(q)
if not self.request.user.has_perm('judge.edit_all_contest'):
q = Q(is_private=False, is_organization_private=False)
if self.request.user.is_authenticated:
q |= Q(is_organization_private=True, organizations__in=self.request.profile.organizations.all())
q |= Q(is_private=True, private_contestants=self.request.profile)
q |= Q(view_contest_scoreboard=self.request.profile)
queryset = queryset.filter(q)
return queryset.distinct()
class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView): class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
model = Contest model = Contest
paginate_by = 20 paginate_by = 20
template_name = 'contest/list.html' template_name = 'contest/list.html'
title = gettext_lazy('Contests') title = gettext_lazy('Contests')
context_object_name = 'past_contests' context_object_name = 'past_contests'
first_page_href = None all_sorts = frozenset(('name', 'user_count', 'start_time'))
default_desc = frozenset(('name', 'user_count'))
default_sort = '-start_time'
@cached_property @cached_property
def _now(self): def _now(self):
@ -101,7 +93,7 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
def _get_queryset(self): def _get_queryset(self):
queryset = super(ContestList, self).get_queryset() \ queryset = super(ContestList, self).get_queryset() \
.order_by('-start_time', 'key').prefetch_related('tags', 'organizations', 'organizers') .prefetch_related('tags', 'organizations', 'authors', 'curators', 'testers')
if 'contest' in self.request.GET: if 'contest' in self.request.GET:
self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip() self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip()
@ -114,7 +106,7 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
return queryset return queryset
def get_queryset(self): def get_queryset(self):
return self._get_queryset().filter(end_time__lt=self._now) return self._get_queryset().order_by(self.order, 'key').filter(end_time__lt=self._now)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ContestList, self).get_context_data(**kwargs) context = super(ContestList, self).get_context_data(**kwargs)
@ -128,12 +120,15 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile, for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile,
contest_id__in=present) \ contest_id__in=present) \
.select_related('contest').prefetch_related('contest__organizers'): .select_related('contest') \
.prefetch_related('contest__authors', 'contest__curators', 'contest__testers')\
.annotate(key=F('contest__key')):
if not participation.ended: if not participation.ended:
active.append(participation) active.append(participation)
present.remove(participation.contest) present.remove(participation.contest)
active.sort(key=attrgetter('end_time')) active.sort(key=attrgetter('end_time', 'key'))
present.sort(key=attrgetter('end_time', 'key'))
future.sort(key=attrgetter('start_time')) future.sort(key=attrgetter('start_time'))
context['active_participations'] = active context['active_participations'] = active
context['current_contests'] = present context['current_contests'] = present
@ -143,9 +138,8 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
context['contest_query'] = self.contest_query context['contest_query'] = self.contest_query
context['org_query'] = self.org_query context['org_query'] = self.org_query
context['organizations'] = Organization.objects.all() context['organizations'] = Organization.objects.all()
context['page_suffix'] = suffix = ( context.update(self.get_sort_context())
'?' + self.request.GET.urlencode()) if self.request.GET else '' context.update(self.get_sort_paginate_context())
context['first_page_href'] = (self.first_page_href or '.') + suffix
return context return context
@ -164,37 +158,44 @@ class ContestMixin(object):
slug_url_kwarg = 'contest' slug_url_kwarg = 'contest'
@cached_property @cached_property
def is_organizer(self): def is_editor(self):
return self.check_organizer() if not self.request.user.is_authenticated:
return False
return self.request.profile.id in self.object.editor_ids
def check_organizer(self, contest=None, user=None): @cached_property
if user is None: def is_tester(self):
user = self.request.user if not self.request.user.is_authenticated:
return (contest or self.object).is_editable_by(user) return False
return self.request.profile.id in self.object.tester_ids
@cached_property
def can_edit(self):
return self.object.is_editable_by(self.request.user)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ContestMixin, self).get_context_data(**kwargs) context = super(ContestMixin, self).get_context_data(**kwargs)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
profile = self.request.profile try:
in_contest = context['in_contest'] = (profile.current_contest is not None and context['live_participation'] = (
profile.current_contest.contest == self.object) self.request.profile.contest_history.get(
if in_contest: contest=self.object,
context['participation'] = profile.current_contest virtual=ContestParticipation.LIVE,
context['participating'] = True )
)
except ContestParticipation.DoesNotExist:
context['live_participation'] = None
context['has_joined'] = False
else: else:
try: context['has_joined'] = True
context['participation'] = profile.contest_history.get(contest=self.object, virtual=0)
except ContestParticipation.DoesNotExist:
context['participating'] = False
context['participation'] = None
else:
context['participating'] = True
else: else:
context['participating'] = False context['live_participation'] = None
context['participation'] = None context['has_joined'] = False
context['in_contest'] = False
context['now'] = timezone.now() context['now'] = timezone.now()
context['is_organizer'] = self.is_organizer context['is_editor'] = self.is_editor
context['is_tester'] = self.is_tester
context['can_edit'] = self.can_edit
if not self.object.og_image or not self.object.summary: if not self.object.og_image or not self.object.summary:
metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id, metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id,
@ -210,18 +211,22 @@ class ContestMixin(object):
def get_object(self, queryset=None): def get_object(self, queryset=None):
contest = super(ContestMixin, self).get_object(queryset) contest = super(ContestMixin, self).get_object(queryset)
user = self.request.user
profile = self.request.profile profile = self.request.profile
if (profile is not None and if (profile is not None and
ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()): ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()):
return contest return contest
if not contest.is_visible and not user.has_perm('judge.see_private_contest') and ( try:
not user.has_perm('judge.edit_own_contest') or contest.access_check(self.request.user)
not self.check_organizer(contest, user)): except Contest.PrivateContest:
raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private,
contest.organizations.all())
except Contest.Inaccessible:
raise Http404() raise Http404()
else:
return contest
if contest.is_private or contest.is_organization_private: if contest.is_private or contest.is_organization_private:
private_contest_error = PrivateContestError(contest.name, contest.is_private, private_contest_error = PrivateContestError(contest.name, contest.is_private,
contest.is_organization_private, contest.organizations.all()) contest.is_organization_private, contest.organizations.all())
@ -297,7 +302,7 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje
contest.organizations.set(organizations) contest.organizations.set(organizations)
contest.private_contestants.set(private_contestants) contest.private_contestants.set(private_contestants)
contest.view_contest_scoreboard.set(view_contest_scoreboard) contest.view_contest_scoreboard.set(view_contest_scoreboard)
contest.organizers.add(self.request.profile) contest.authors.add(self.request.profile)
for problem in contest_problems: for problem in contest_problems:
problem.contest = contest problem.contest = contest
@ -337,7 +342,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
def join_contest(self, request, access_code=None): def join_contest(self, request, access_code=None):
contest = self.object contest = self.object
if not contest.can_join and not self.is_organizer: if not contest.can_join and not (self.is_editor or self.is_tester):
return generic_message(request, _('Contest not ongoing'), return generic_message(request, _('Contest not ongoing'),
_('"%s" is not currently ongoing.') % contest.name) _('"%s" is not currently ongoing.') % contest.name)
@ -351,8 +356,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
_('You have been declared persona non grata for this contest. ' _('You have been declared persona non grata for this contest. '
'You are permanently barred from joining this contest.')) 'You are permanently barred from joining this contest.'))
requires_access_code = (not (request.user.is_superuser or self.is_organizer) and requires_access_code = (not self.can_edit and contest.access_code and access_code != contest.access_code)
contest.access_code and access_code != contest.access_code)
if contest.ended: if contest.ended:
if requires_access_code: if requires_access_code:
raise ContestAccessDenied() raise ContestAccessDenied()
@ -371,22 +375,24 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
else: else:
break break
else: else:
SPECTATE = ContestParticipation.SPECTATE
LIVE = ContestParticipation.LIVE
try: try:
participation = ContestParticipation.objects.get( participation = ContestParticipation.objects.get(
contest=contest, user=profile, virtual=(-1 if self.is_organizer else 0), contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
) )
except ContestParticipation.DoesNotExist: except ContestParticipation.DoesNotExist:
if requires_access_code: if requires_access_code:
raise ContestAccessDenied() raise ContestAccessDenied()
participation = ContestParticipation.objects.create( participation = ContestParticipation.objects.create(
contest=contest, user=profile, virtual=(-1 if self.is_organizer else 0), contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
real_start=timezone.now(), real_start=timezone.now(),
) )
else: else:
if participation.ended: if participation.ended:
participation = ContestParticipation.objects.get_or_create( participation = ContestParticipation.objects.get_or_create(
contest=contest, user=profile, virtual=-1, contest=contest, user=profile, virtual=SPECTATE,
defaults={'real_start': timezone.now()}, defaults={'real_start': timezone.now()},
)[0] )[0]
@ -449,7 +455,7 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
def get_contest_data(self, start, end): def get_contest_data(self, start, end):
end += timedelta(days=1) end += timedelta(days=1)
contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) | contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) |
Q(end_time__gte=start, end_time__lt=end)).defer('description') Q(end_time__gte=start, end_time__lt=end))
starts, ends, oneday = (defaultdict(list) for i in range(3)) starts, ends, oneday = (defaultdict(list) for i in range(3))
for contest in contests: for contest in contests:
start_date = timezone.localtime(contest.start_time).date() start_date = timezone.localtime(contest.start_time).date()
@ -523,6 +529,7 @@ class CachedContestCalendar(ContestCalendar):
class ContestStats(TitleMixin, ContestMixin, DetailView): class ContestStats(TitleMixin, ContestMixin, DetailView):
template_name = 'contest/stats.html' template_name = 'contest/stats.html'
POINT_BIN = 10 # in point distribution
def get_title(self): def get_title(self):
return _('%s Statistics') % self.object.name return _('%s Statistics') % self.object.name
@ -530,7 +537,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if not (self.object.ended or self.object.is_editable_by(self.request.user)): if not (self.object.ended or self.can_edit):
raise Http404() raise Http404()
queryset = Submission.objects.filter(contest_object=self.object) queryset = Submission.objects.filter(contest_object=self.object)
@ -542,9 +549,10 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
queryset.values('problem__code', 'result').annotate(count=Count('result')) queryset.values('problem__code', 'result').annotate(count=Count('result'))
.values_list('problem__code', 'result', 'count'), .values_list('problem__code', 'result', 'count'),
) )
labels, codes = zip( labels, codes = [], []
*self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code'), contest_problems = self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code')
) if contest_problems:
labels, codes = zip(*contest_problems)
num_problems = len(labels) num_problems = len(labels)
status_counts = [[] for i in range(num_problems)] status_counts = [[] for i in range(num_problems)]
for problem_code, result, count in status_count_queryset: for problem_code, result, count in status_count_queryset:
@ -556,6 +564,21 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']: for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']:
result_data[category['code']][i] = category['count'] result_data[category['code']][i] = category['count']
problem_points = [[] for _ in range(num_problems)]
point_count_queryset = list(queryset.values('problem__code', 'contest__points', 'contest__problem__points')
.annotate(count=Count('contest__points'))
.order_by('problem__code', 'contest__points')
.values_list('problem__code', 'contest__points', 'contest__problem__points', 'count'))
counter = [[0 for _ in range(self.POINT_BIN + 1)] for _ in range(num_problems)]
for problem_code, point, max_point, count in point_count_queryset:
if (point == None) or (problem_code not in codes): continue
problem_idx = codes.index(problem_code)
bin_idx = math.floor(point * self.POINT_BIN / max_point)
counter[problem_idx][bin_idx] += count
for i in range(num_problems):
problem_points[i] = [(j * 100 / self.POINT_BIN, counter[i][j])
for j in range(len(counter[i]))]
stats = { stats = {
'problem_status_count': { 'problem_status_count': {
'labels': labels, 'labels': labels,
@ -572,6 +595,9 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate) queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate)
.order_by('contest__problem__order').values_list('problem__name', 'ac_rate'), .order_by('contest__problem__order').values_list('problem__name', 'ac_rate'),
), ),
'problem_point': [get_histogram(problem_points[i])
for i in range(num_problems)
],
'language_count': get_pie_chart( 'language_count': get_pie_chart(
queryset.values('language__name').annotate(count=Count('language__name')) queryset.values('language__name').annotate(count=Count('language__name'))
.filter(count__gt=0).order_by('-count').values_list('language__name', 'count'), .filter(count__gt=0).order_by('-count').values_list('language__name', 'count'),
@ -583,13 +609,13 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
} }
context['stats'] = mark_safe(json.dumps(stats)) context['stats'] = mark_safe(json.dumps(stats))
context['problems'] = labels
return context return context
ContestRankingProfile = namedtuple( ContestRankingProfile = namedtuple(
'ContestRankingProfile', 'ContestRankingProfile',
'id user css_class username points cumtime organization participation ' 'id user css_class username points cumtime tiebreaker organization participation '
'participation_rating problem_cells result_cell', 'participation_rating problem_cells result_cell',
) )
@ -605,6 +631,7 @@ def make_contest_ranking_profile(contest, participation, contest_problems):
username=user.username, username=user.username,
points=participation.score, points=participation.score,
cumtime=participation.cumtime, cumtime=participation.cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization, organization=user.organization,
participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None, participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None,
problem_cells=[contest.format.display_user_problem(participation, contest_problem) problem_cells=[contest.format.display_user_problem(participation, contest_problem)
@ -619,22 +646,20 @@ def base_contest_ranking_list(contest, problems, queryset):
queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')] queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')]
def contest_ranking_list(contest, problems): def contest_ranking_list(contest, problems, queryset=None):
return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0, user__is_unlisted=False) if not queryset:
queryset = contest.users.filter(virtual=0)
return base_contest_ranking_list(contest, problems, queryset
.prefetch_related('user__organizations') .prefetch_related('user__organizations')
.extra(select={'round_score': 'round(score, 6)'}) .extra(select={'round_score': 'round(score, 6)'})
.order_by('is_disqualified', '-round_score', 'cumtime')) .order_by('is_disqualified', '-round_score', 'cumtime', 'tiebreaker'))
def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list, def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list,
show_current_virtual=True, ranker=ranker): show_current_virtual=False, ranker=ranker):
problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order')) problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order'))
if contest.hide_scoreboard and contest.is_in_contest(request.user): users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker'))
return ([(_('???'), make_contest_ranking_profile(contest, request.profile.current_contest, problems))],
problems)
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:
@ -651,15 +676,24 @@ def contest_ranking_ajax(request, contest, participation=None):
if not exists: if not exists:
return HttpResponseBadRequest('Invalid contest', content_type='text/plain') return HttpResponseBadRequest('Invalid contest', content_type='text/plain')
if not contest.can_see_scoreboard(request.user): if not contest.can_see_full_scoreboard(request.user):
raise Http404() raise Http404()
users, problems = get_contest_ranking_list(request, contest, participation) queryset = contest.users.filter(virtual__gte=0)
if request.GET.get('friend') == 'true' and request.profile:
friends = list(request.profile.get_friends())
queryset = queryset.filter(user__user__username__in=friends)
if request.GET.get('virtual') != 'true':
queryset = queryset.filter(virtual=0)
users, problems = get_contest_ranking_list(request, contest, participation,
ranking_list=partial(contest_ranking_list, queryset=queryset))
return render(request, 'contest/ranking-table.html', { return render(request, 'contest/ranking-table.html', {
'users': users, 'users': users,
'problems': problems, 'problems': problems,
'contest': contest, 'contest': contest,
'has_rating': contest.ratings.exists(), 'has_rating': contest.ratings.exists(),
'can_edit': contest.is_editable_by(request.user)
}) })
@ -679,13 +713,12 @@ class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if not self.object.can_see_scoreboard(self.request.user): if not self.object.can_see_own_scoreboard(self.request.user):
raise Http404() raise Http404()
users, problems = self.get_ranking_list() users, problems = self.get_ranking_list()
context['users'] = users context['users'] = users
context['problems'] = problems context['problems'] = problems
context['last_msg'] = event.last()
context['tab'] = self.tab context['tab'] = self.tab
return context return context
@ -697,6 +730,14 @@ class ContestRanking(ContestRankingBase):
return _('%s Rankings') % self.object.name return _('%s Rankings') % self.object.name
def get_ranking_list(self): def get_ranking_list(self):
if not self.object.can_see_full_scoreboard(self.request.user):
queryset = self.object.users.filter(user=self.request.profile, virtual=ContestParticipation.LIVE)
return get_contest_ranking_list(
self.request, self.object,
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
ranker=lambda users, key: ((_('???'), user) for user in users),
)
return get_contest_ranking_list(self.request, self.object) return get_contest_ranking_list(self.request, self.object)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -714,6 +755,9 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
return _("%s's participation in %s") % (self.profile.username, self.object.name) return _("%s's participation in %s") % (self.profile.username, self.object.name)
def get_ranking_list(self): def get_ranking_list(self):
if not self.object.can_see_full_scoreboard(self.request.user) and self.profile != self.request.profile:
raise Http404()
queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual') queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual')
live_link = format_html('<a href="{2}#!{1}">{0}</a>', _('Live'), self.profile.username, live_link = format_html('<a href="{2}#!{1}">{0}</a>', _('Live'), self.profile.username,
reverse('contest_ranking', args=[self.object.key])) reverse('contest_ranking', args=[self.object.key]))
@ -728,6 +772,7 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
context['has_rating'] = False context['has_rating'] = False
context['now'] = timezone.now() context['now'] = timezone.now()
context['rank_header'] = _('Participation') context['rank_header'] = _('Participation')
context['participation_tab'] = True
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -762,7 +807,7 @@ class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
def get_object(self, queryset=None): def get_object(self, queryset=None):
contest = super().get_object(queryset) contest = super().get_object(queryset)
if settings.MOSS_API_KEY is None: if settings.MOSS_API_KEY is None or not contest.is_editable_by(self.request.user):
raise Http404() raise Http404()
if not contest.is_editable_by(self.request.user): if not contest.is_editable_by(self.request.user):
raise Http404() raise Http404()
@ -824,3 +869,87 @@ class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
def get_title(self): def get_title(self):
return _('Contest tag: %s') % self.object.name return _('Contest tag: %s') % self.object.name
class ProblemClarificationForm(forms.Form):
body = forms.CharField(widget=HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
preview_timeout=1000, hide_preview_button=True))
def __init__(self, request, *args, **kwargs):
self.request = request
super(ProblemClarificationForm, self).__init__(*args, **kwargs)
self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')})
class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView):
form_class = ProblemClarificationForm
template_name = 'contest/clarification.html'
def get_form_kwargs(self):
kwargs = super(NewContestClarificationView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def is_accessible(self):
if not self.request.user.is_authenticated:
return False
if not self.request.in_contest:
return False
if not self.request.participation.contest == self.get_object():
return False
return self.request.user.is_superuser or \
self.request.profile in self.request.participation.contest.authors.all() or \
self.request.profile in self.request.participation.contest.curators.all()
def get(self, request, *args, **kwargs):
if not self.is_accessible():
raise Http404()
return super().get(self, request, *args, **kwargs)
def form_valid(self, form):
problem_code = self.request.POST['problem']
description = form.cleaned_data['body']
clarification = ProblemClarification(description=description)
clarification.problem = Problem.objects.get(code=problem_code)
clarification.save()
link = reverse('home')
return HttpResponseRedirect(link)
def get_title(self):
return "New clarification for %s" % self.object.name
def get_content_title(self):
return mark_safe(escape(_('New clarification for %s')) %
format_html('<a href="{0}">{1}</a>', reverse('problem_detail', args=[self.object.key]),
self.object.name))
def get_context_data(self, **kwargs):
context = super(NewContestClarificationView, self).get_context_data(**kwargs)
context['problems'] = ContestProblem.objects.filter(contest=self.object)\
.order_by('order')
return context
class ContestClarificationAjax(ContestMixin, DetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.is_accessible_by(request.user):
raise Http404()
polling_time = 1 # minute
last_one_minute = last_five_minutes = timezone.now()-timezone.timedelta(minutes=polling_time)
queryset = list(ProblemClarification.objects.filter(
problem__in=self.object.problems.all(),
date__gte=last_one_minute
).values('problem', 'problem__name', 'description'))
problems = list(ContestProblem.objects.filter(contest=self.object)\
.order_by('order').values('problem'))
problems = [i['problem'] for i in problems]
for cla in queryset:
cla['order'] = self.object.get_label_for_problem(problems.index(cla['problem']))
return JsonResponse(queryset, safe=False, json_dumps_params={'ensure_ascii': False})

View file

@ -11,6 +11,7 @@ from django.forms import Form, modelformset_factory
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy, ungettext from django.utils.translation import gettext as _, gettext_lazy, ungettext
from django.views.generic import DetailView, FormView, ListView, UpdateView, View from django.views.generic import DetailView, FormView, ListView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
@ -114,6 +115,7 @@ class OrganizationHome(OrganizationDetailView):
Comment.objects.filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False) Comment.objects.filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False)
.values_list('page').annotate(count=Count('page')).order_by() .values_list('page').annotate(count=Count('page')).order_by()
} }
context['pending_count'] = OrganizationRequest.objects.filter(state='P', organization=self.object).count()
return context return context
@ -230,7 +232,7 @@ class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView):
OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True) OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True)
class OrganizationRequestBaseView(LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View): class OrganizationRequestBaseView(TitleMixin, LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View):
model = Organization model = Organization
slug_field = 'key' slug_field = 'key'
slug_url_kwarg = 'key' slug_url_kwarg = 'key'
@ -243,6 +245,10 @@ class OrganizationRequestBaseView(LoginRequiredMixin, SingleObjectTemplateRespon
raise PermissionDenied() raise PermissionDenied()
return organization return organization
def get_content_title(self):
href = reverse('organization_home', args=[self.object.id, self.object.slug])
return mark_safe(f'Manage join requests for <a href="{href}">{self.object.name}</a>')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs) context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs)
context['title'] = _('Managing join requests for %s') % self.object.name context['title'] = _('Managing join requests for %s') % self.object.name

View file

@ -21,14 +21,14 @@ from django.utils.functional import cached_property
from django.utils.html import escape, format_html from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy from django.utils.translation import gettext as _, gettext_lazy
from django.views.generic import ListView, View from django.views.generic import DetailView, ListView, View
from django.views.generic.base import TemplateResponseMixin from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from judge.comments import CommentedDetailView from judge.comments import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm from judge.forms import ProblemCloneForm, ProblemSubmitForm
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemGroup, \ from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \
ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \ ProblemGroup, ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization TranslatedProblemForeignKeyQuerySet, Organization
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
@ -154,13 +154,10 @@ class ProblemRaw(ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMi
)) ))
class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView):
context_object_name = 'problem' context_object_name = 'problem'
template_name = 'problem/problem.html' template_name = 'problem/problem.html'
def get_comment_page(self):
return 'p:%s' % self.object.code
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProblemDetail, self).get_context_data(**kwargs) context = super(ProblemDetail, self).get_context_data(**kwargs)
user = self.request.user user = self.request.user
@ -170,6 +167,7 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
contest_problem = (None if not authed or user.profile.current_contest is None else contest_problem = (None if not authed or user.profile.current_contest is None else
get_contest_problem(self.object, user.profile)) get_contest_problem(self.object, user.profile))
context['contest_problem'] = contest_problem context['contest_problem'] = contest_problem
if contest_problem: if contest_problem:
clarifications = self.object.clarifications clarifications = self.object.clarifications
context['has_clarifications'] = clarifications.count() > 0 context['has_clarifications'] = clarifications.count() > 0
@ -220,6 +218,21 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
return context return context
class ProblemComments(ProblemMixin, TitleMixin, CommentedDetailView):
context_object_name = 'problem'
template_name = 'problem/comments.html'
def get_title(self):
return _('Disscuss {0}').format(self.object.name)
def get_content_title(self):
return format_html(_(u'Discuss <a href="{1}">{0}</a>'), self.object.name,
reverse('problem_detail', args=[self.object.code]))
def get_comment_page(self):
return 'p:%s' % self.object.code
class LatexError(Exception): class LatexError(Exception):
pass pass
@ -256,7 +269,6 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
'math_engine': maker.math_engine, 'math_engine': maker.math_engine,
}).replace('"//', '"https://').replace("'//", "'https://") }).replace('"//', '"https://').replace("'//", "'https://")
maker.title = problem_name maker.title = problem_name
assets = ['style.css', 'pygment-github.css'] assets = ['style.css', 'pygment-github.css']
if maker.math_engine == 'jax': if maker.math_engine == 'jax':
assets.append('mathjax_config.js') assets.append('mathjax_config.js')
@ -267,7 +279,6 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
self.logger.error('Failed to render PDF for %s', problem.code) self.logger.error('Failed to render PDF for %s', problem.code)
return HttpResponse(maker.log, status=500, content_type='text/plain') return HttpResponse(maker.log, status=500, content_type='text/plain')
shutil.move(maker.pdffile, cache) shutil.move(maker.pdffile, cache)
response = HttpResponse() response = HttpResponse()
if hasattr(settings, 'DMOJ_PDF_PROBLEM_INTERNAL') and \ if hasattr(settings, 'DMOJ_PDF_PROBLEM_INTERNAL') and \
request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'): request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'):
@ -438,7 +449,17 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
else: else:
context['hot_problems'] = None context['hot_problems'] = None
context['point_start'], context['point_end'], context['point_values'] = 0, 0, {} context['point_start'], context['point_end'], context['point_values'] = 0, 0, {}
context['hide_contest_scoreboard'] = self.contest.hide_scoreboard context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \
(self.contest.SCOREBOARD_AFTER_CONTEST, self.contest.SCOREBOARD_AFTER_PARTICIPATION)
context['has_clarifications'] = False
if self.request.user.is_authenticated:
participation = self.request.profile.current_contest
if participation:
clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all())
context['has_clarifications'] = clarifications.count() > 0
context['clarifications'] = clarifications.order_by('-date')
if participation.contest.is_editable_by(self.request.user):
context['can_edit_contest'] = True
return context return context
def get_noui_slider_points(self): def get_noui_slider_points(self):

View file

@ -2,14 +2,24 @@ import json
import mimetypes import mimetypes
import os import os
from itertools import chain from itertools import chain
import shutil
from tempfile import gettempdir
from zipfile import BadZipfile, ZipFile from zipfile import BadZipfile, ZipFile
from django import forms
from django.conf import settings
from django.http import HttpResponse, HttpRequest
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.files import File
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import BaseModelFormSet, HiddenInput, ModelForm, NumberInput, Select, formset_factory, FileInput from django.forms import BaseModelFormSet, HiddenInput, ModelForm, NumberInput, Select, formset_factory, FileInput
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape, format_html from django.utils.html import escape, format_html
@ -22,6 +32,7 @@ from judge.models import Problem, ProblemData, ProblemTestCase, Submission, prob
from judge.utils.problem_data import ProblemDataCompiler from judge.utils.problem_data import ProblemDataCompiler
from judge.utils.unicode import utf8text from judge.utils.unicode import utf8text
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.utils.fine_uploader import combine_chunks, save_upload, handle_upload, FineUploadFileInput, FineUploadForm
from judge.views.problem import ProblemMixin from judge.views.problem import ProblemMixin
mimetypes.init() mimetypes.init()
@ -52,6 +63,7 @@ class ProblemDataForm(ModelForm):
model = ProblemData model = ProblemData
fields = ['zipfile', 'checker', 'checker_args', 'custom_checker', 'custom_validator'] fields = ['zipfile', 'checker', 'checker_args', 'custom_checker', 'custom_validator']
widgets = { widgets = {
'zipfile': FineUploadFileInput,
'checker_args': HiddenInput, 'checker_args': HiddenInput,
'generator': HiddenInput, 'generator': HiddenInput,
'output_limit': HiddenInput, 'output_limit': HiddenInput,
@ -76,6 +88,7 @@ class ProblemCaseForm(ModelForm):
} }
class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1,
can_delete=True)): can_delete=True)):
model = ProblemTestCase model = ProblemTestCase
@ -242,3 +255,39 @@ def problem_init_view(request, problem):
format_html('<a href="{1}">{0}</a>', problem.name, format_html('<a href="{1}">{0}</a>', problem.name,
reverse('problem_detail', args=[problem.code])))), reverse('problem_detail', args=[problem.code])))),
}) })
class ProblemZipUploadView(ProblemManagerMixin, View):
def dispatch(self, *args, **kwargs):
return super(ProblemZipUploadView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = problem = self.get_object()
problem_data = get_object_or_404(ProblemData, problem=self.object)
form = FineUploadForm(request.POST, request.FILES)
if form.is_valid():
fileuid = form.cleaned_data['qquuid']
filename = form.cleaned_data['qqfilename']
dest = os.path.join(gettempdir(), fileuid)
def post_upload():
zip_dest = os.path.join(dest, filename)
try:
ZipFile(zip_dest).namelist() # check if this file is valid
with open(zip_dest, 'rb') as f:
problem_data.zipfile.delete()
problem_data.zipfile.save(filename, File(f))
f.close()
except Exception as e:
raise Exception(e)
finally:
shutil.rmtree(dest)
try:
handle_upload(request.FILES['qqfile'], form.cleaned_data, dest, post_upload=post_upload)
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
return JsonResponse({'success': True})
else:
return HttpResponse(status_code=400)

View file

@ -40,6 +40,16 @@ class CustomRegistrationForm(RegistrationForm):
if ReCaptchaField is not None: if ReCaptchaField is not None:
captcha = ReCaptchaField(widget=ReCaptchaWidget()) captcha = ReCaptchaField(widget=ReCaptchaWidget())
def clean_organizations(self):
organizations = self.cleaned_data.get('organizations') or []
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if sum(org.is_open for org in organizations) > max_orgs:
raise forms.ValidationError(
_('You may not be part of more than {count} public organizations.').format(count=max_orgs))
return self.cleaned_data['organizations']
def clean_email(self): def clean_email(self):
if User.objects.filter(email=self.cleaned_data['email']).exists(): if User.objects.filter(email=self.cleaned_data['email']).exists():
raise forms.ValidationError(gettext('The email address "%s" is already taken. Only one registration ' raise forms.ValidationError(gettext('The email address "%s" is already taken. Only one registration '
@ -71,7 +81,7 @@ class RegistrationView(OldRegistrationView):
def register(self, form): def register(self, form):
user = super(RegistrationView, self).register(form) user = super(RegistrationView, self).register(form)
profile, _ = Profile.objects.get_or_create(user=user, defaults={ profile, _ = Profile.objects.get_or_create(user=user, defaults={
'language': Language.get_python3(), 'language': Language.get_default_language(),
}) })
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data

View file

@ -4,6 +4,8 @@ from django.shortcuts import get_object_or_404
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.views.generic.list import BaseListView from django.views.generic.list import BaseListView
from chat_box.utils import encrypt_url
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
from judge.models import Comment, Contest, Organization, Problem, Profile from judge.models import Comment, Contest, Organization, Problem, Profile
@ -54,29 +56,14 @@ class OrganizationSelect2View(Select2View):
class ProblemSelect2View(Select2View): class ProblemSelect2View(Select2View):
def get_queryset(self): def get_queryset(self):
queryset = Problem.objects.filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)) return Problem.get_visible_problems(self.request.user) \
if not self.request.user.has_perm('judge.see_private_problem'): .filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)).distinct()
filter = Q(is_public=True)
if self.request.user.is_authenticated:
filter |= Q(authors=self.request.profile) | Q(curators=self.request.profile)
queryset = queryset.filter(filter).distinct()
return queryset.distinct()
class ContestSelect2View(Select2View): class ContestSelect2View(Select2View):
def get_queryset(self): def get_queryset(self):
queryset = Contest.objects.filter(Q(key__icontains=self.term) | Q(name__icontains=self.term)) return Contest.get_visible_contests(self.request.user) \
if not self.request.user.has_perm('judge.see_private_contest'): .filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
queryset = queryset.filter(is_visible=True)
if not self.request.user.has_perm('judge.edit_all_contest'):
q = Q(is_private=False, is_organization_private=False)
if self.request.user.is_authenticated:
q |= Q(is_organization_private=True,
organizations__in=self.request.profile.organizations.all())
q |= Q(is_private=True, private_contestants=self.request.profile)
q |= Q(view_contest_scoreboard=self.request.profile)
queryset = queryset.filter(q)
return queryset
class CommentSelect2View(Select2View): class CommentSelect2View(Select2View):
@ -119,8 +106,7 @@ class UserSearchSelect2View(BaseListView):
class ContestUserSearchSelect2View(UserSearchSelect2View): class ContestUserSearchSelect2View(UserSearchSelect2View):
def get_queryset(self): def get_queryset(self):
contest = get_object_or_404(Contest, key=self.kwargs['contest']) contest = get_object_or_404(Contest, key=self.kwargs['contest'])
if not contest.can_see_scoreboard(self.request.user) or \ if not contest.is_accessible_by(self.request.user) or not contest.can_see_full_scoreboard(self.request.user):
contest.hide_scoreboard and contest.is_in_contest(self.request.user):
raise Http404() raise Http404()
return Profile.objects.filter(contest_history__contest=contest, return Profile.objects.filter(contest_history__contest=contest,
@ -137,3 +123,35 @@ class AssigneeSelect2View(UserSearchSelect2View):
def get_queryset(self): def get_queryset(self):
return Profile.objects.filter(assigned_tickets__isnull=False, return Profile.objects.filter(assigned_tickets__isnull=False,
user__username__icontains=self.term).distinct() user__username__icontains=self.term).distinct()
class ChatUserSearchSelect2View(BaseListView):
paginate_by = 20
def get_queryset(self): # TODO: add block
return _get_user_queryset(self.term)
def get(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs
self.term = kwargs.get('term', request.GET.get('term', ''))
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')
context = self.get_context_data()
return JsonResponse({
'results': [
{
'text': username,
'id': encrypt_url(request.profile.id, pk),
'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default),
'display_rank': display_rank,
} for pk, username, email, display_rank in context['object_list']],
'more': context['page_obj'].has_next(),
})
def get_name(self, obj):
return str(obj)

View file

@ -43,7 +43,8 @@ from judge.utils.problems import get_result_data
from judge.utils.problems import user_authored_ids from judge.utils.problems import user_authored_ids
from judge.utils.problems import user_completed_ids from judge.utils.problems import user_completed_ids
from judge.utils.problems import user_editable_ids from judge.utils.problems import user_editable_ids
from judge.utils.raw_sql import use_straight_join from judge.utils.problem_data import get_problem_case
from judge.utils.raw_sql import join_sql_subquery, use_straight_join
from judge.utils.views import DiggPaginatorMixin from judge.utils.views import DiggPaginatorMixin
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
@ -110,7 +111,7 @@ class SubmissionSource(SubmissionDetailBase):
submission = self.object submission = self.object
context['raw_source'] = submission.source.source.rstrip('\n') context['raw_source'] = submission.source.source.rstrip('\n')
context['highlighted_source'] = highlight_code( context['highlighted_source'] = highlight_code(
submission.source.source, submission.language.pygments) submission.source.source, submission.language.pygments, linenos=False)
return context return context
@ -137,60 +138,28 @@ def group_test_cases(cases):
return result return result
def read_head_archive(archive, file): def get_cases_data(submission):
with archive.open(file) as f:
s = f.read(settings.TESTCASE_VISIBLE_LENGTH + 3)
# add this so there are no characters left behind (ex, 'á' = 2 utf-8 chars)
while True:
try:
s.decode('utf-8')
break
except UnicodeDecodeError:
s += f.read(1)
return s
def get_visible_content(data):
data = data or b''
data = data.replace(b'\r\n', b'\r').replace(b'\r', b'\n')
data = data.decode('utf-8')
if (len(data) > settings.TESTCASE_VISIBLE_LENGTH):
data = data[:settings.TESTCASE_VISIBLE_LENGTH]
data += '.' * 3
return data
def get_input_answer(case, archive):
result = {'input': '', 'answer': ''}
if (len(case.input_file)):
result['input'] = get_visible_content(read_head_archive(archive, case.input_file))
if (len(case.output_file)):
result['answer'] = get_visible_content(read_head_archive(archive, case.output_file))
return result
def get_problem_data(submission):
archive_path = os.path.join(settings.DMOJ_PROBLEM_DATA_ROOT,
str(submission.problem.data_files.zipfile))
if not os.path.exists(archive_path):
raise Exception(
'archive file "%s" does not exist' % archive_path)
try:
archive = zipfile.ZipFile(archive_path, 'r')
except zipfile.BadZipfile:
raise Exception('bad archive: "%s"' % archive_path)
testcases = ProblemTestCase.objects.filter(dataset=submission.problem)\ testcases = ProblemTestCase.objects.filter(dataset=submission.problem)\
.order_by('order') .order_by('order')
if (submission.is_pretested): if (submission.is_pretested):
testcases = testcases.filter(is_pretest=True) testcases = testcases.filter(is_pretest=True)
files = []
for case in testcases:
if case.input_file: files.append(case.input_file)
if case.output_file: files.append(case.output_file)
case_data = get_problem_case(submission.problem, files)
problem_data = {} problem_data = {}
for count, case in enumerate(testcases): count = 0
problem_data[count + 1] = get_input_answer(case, archive) for case in testcases:
if case.type != 'C': continue
count += 1
problem_data[count] = {
'input': case_data[case.input_file] if case.input_file else '',
'answer': case_data[case.output_file] if case.output_file else '',
}
return problem_data return problem_data
@ -198,20 +167,36 @@ def get_problem_data(submission):
class SubmissionStatus(SubmissionDetailBase): class SubmissionStatus(SubmissionDetailBase):
template_name = 'submission/status.html' template_name = 'submission/status.html'
def access_testcases_in_contest(self):
contest = self.object.contest_or_none
if contest is None:
return False
if contest.problem.problem.is_editable_by(self.request.user):
return True
if contest.problem.contest.is_in_contest(self.request.user):
return False
if contest.participation.ended:
return True
return False
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SubmissionStatus, self).get_context_data(**kwargs) context = super(SubmissionStatus, self).get_context_data(**kwargs)
submission = self.object submission = self.object
context['last_msg'] = event.last() context['last_msg'] = event.last()
context['batches'] = group_test_cases(submission.test_cases.all()) context['batches'] = group_test_cases(submission.test_cases.all())
context['time_limit'] = submission.problem.time_limit context['time_limit'] = submission.problem.time_limit
context['can_see_testcases'] = False
contest = submission.contest_or_none contest = submission.contest_or_none
prefix_length = 0 prefix_length = 0
can_see_testcases = self.access_testcases_in_contest()
if (contest is not None): if (contest is not None):
prefix_length = contest.problem.output_prefix_override prefix_length = contest.problem.output_prefix_override
if ((contest is None or prefix_length > 0) or self.request.user.is_superuser):
context['cases_data'] = get_problem_data(submission) if contest is None or prefix_length > 0 or can_see_testcases:
context['cases_data'] = get_cases_data(submission)
context['can_see_testcases'] = True
try: try:
lang_limit = submission.problem.language_limits.get( lang_limit = submission.problem.language_limits.get(
language=submission.language) language=submission.language)
@ -292,11 +277,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
queryset=ProblemTranslation.objects.filter( queryset=ProblemTranslation.objects.filter(
language=self.request.LANGUAGE_CODE), to_attr='_trans')) language=self.request.LANGUAGE_CODE), to_attr='_trans'))
if self.in_contest: if self.in_contest:
queryset = queryset.filter( queryset = queryset.filter(contest_object=self.contest)
contest__participation__contest_id=self.contest.id) if not self.contest.can_see_full_scoreboard(self.request.user):
if self.contest.hide_scoreboard and self.contest.is_in_contest(self.request.user): queryset = queryset.filter(user=self.request.profile)
queryset = queryset.filter(
contest__participation__user=self.request.profile)
else: else:
queryset = queryset.select_related( queryset = queryset.select_related(
'contest_object').defer('contest_object__description') 'contest_object').defer('contest_object__description')
@ -304,12 +287,18 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
# This is not technically correct since contest organizers *should* see these, but # This is not technically correct since contest organizers *should* see these, but
# the join would be far too messy # the join would be far too messy
if not self.request.user.has_perm('judge.see_private_contest'): if not self.request.user.has_perm('judge.see_private_contest'):
queryset = queryset.exclude( # Show submissions for any contest you can edit or visible scoreboard
contest_object_id__in=Contest.objects.filter(hide_scoreboard=True)) contest_queryset = Contest.objects.filter(Q(authors=self.request.profile) |
Q(curators=self.request.profile) |
Q(scoreboard_visibility=Contest.SCOREBOARD_VISIBLE) |
Q(end_time__lt=timezone.now())).distinct()
queryset = queryset.filter(Q(user=self.request.profile) |
Q(contest_object__in=contest_queryset) |
Q(contest_object__isnull=True))
if self.selected_languages: if self.selected_languages:
queryset = queryset.filter( queryset = queryset.filter(
language_id__in=Language.objects.filter(key__in=self.selected_languages)) language__in=Language.objects.filter(key__in=self.selected_languages))
if self.selected_statuses: if self.selected_statuses:
queryset = queryset.filter(result__in=self.selected_statuses) queryset = queryset.filter(result__in=self.selected_statuses)
@ -318,14 +307,13 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
def get_queryset(self): def get_queryset(self):
queryset = self._get_queryset() queryset = self._get_queryset()
if not self.in_contest: if not self.in_contest:
if not self.request.user.has_perm('judge.see_private_problem'): join_sql_subquery(
queryset = queryset.filter(problem__is_public=True) queryset,
if not self.request.user.has_perm('judge.see_organization_problem'): subquery=str(Problem.get_visible_problems(self.request.user).distinct().only('id').query),
filter = Q(problem__is_organization_private=False) params=[],
if self.request.user.is_authenticated: join_fields=[('problem_id', 'id')],
filter |= Q( alias='visible_problems',
problem__organizations__in=self.request.profile.organizations.all()) )
queryset = queryset.filter(filter)
return queryset return queryset
def get_my_submissions_page(self): def get_my_submissions_page(self):
@ -452,7 +440,7 @@ class ProblemSubmissionsBase(SubmissionsListBase):
reverse('problem_detail', args=[self.problem.code])) reverse('problem_detail', args=[self.problem.code]))
def access_check_contest(self, request): def access_check_contest(self, request):
if self.in_contest and not self.contest.can_see_scoreboard(request.user): if self.in_contest and not self.contest.can_see_own_scoreboard(request.user):
raise Http404() raise Http404()
def access_check(self, request): def access_check(self, request):

View file

@ -13,7 +13,8 @@ from django.db import transaction
from django.db.models import Count, Max, Min from django.db.models import Count, Max, Min
from django.db.models.fields import DateField from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractYear from django.db.models.functions import Cast, ExtractYear
from django.http import Http404, HttpResponseRedirect, JsonResponse from django.forms import Form
from django.http import Http404, HttpResponseRedirect, JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -21,18 +22,21 @@ from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy from django.utils.translation import gettext as _, gettext_lazy
from django.views import View
from django.views.generic import DetailView, ListView, TemplateView from django.views.generic import DetailView, ListView, TemplateView
from django.template.loader import render_to_string
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, Friend 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.tasks import import_users
from judge.utils.problems import contest_completed_ids, user_completed_ids from judge.utils.problems import contest_completed_ids, user_completed_ids
from judge.utils.ranker import ranker from judge.utils.ranker import ranker
from judge.utils.subscription import Subscription from judge.utils.subscription import Subscription
from judge.utils.unicode import utf8text from judge.utils.unicode import utf8text
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message, SingleObjectFormView
from .contests import ContestRanking from .contests import ContestRanking
__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile'] __all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile']
@ -74,6 +78,11 @@ class UserPage(TitleMixin, UserMixin, DetailView):
return (_('My account') if self.request.user == self.object.user else return (_('My account') if self.request.user == self.object.user else
_('User %s') % self.object.user.username) _('User %s') % self.object.user.username)
def get_content_title(self):
username = self.object.user.username
css_class = self.object.css_class
return mark_safe(f'<span class="{css_class}">{username}</span>')
# TODO: the same code exists in problem.py, maybe move to problems.py? # TODO: the same code exists in problem.py, maybe move to problems.py?
@cached_property @cached_property
def profile(self): def profile(self):
@ -126,6 +135,28 @@ EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
class UserAboutPage(UserPage): class UserAboutPage(UserPage):
template_name = 'user/user-about.html' template_name = 'user/user-about.html'
def get_awards(self, ratings):
result = {}
sorted_ratings = sorted(ratings,
key=lambda x: (x.rank, -x.contest.end_time.timestamp()))
result['medals'] = [{
'label': rating.contest.name,
'ranking': rating.rank,
'link': reverse('contest_ranking', args=(rating.contest.key,)) + '#!' + self.object.username,
'date': date_format(rating.contest.end_time, _('M j, Y')),
} for rating in sorted_ratings if rating.rank <= 3]
num_awards = 0
for i in result:
num_awards += len(result[i])
if num_awards == 0:
result = None
return result
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(UserAboutPage, self).get_context_data(**kwargs) context = super(UserAboutPage, self).get_context_data(**kwargs)
ratings = context['ratings'] = self.object.ratings.order_by('-contest__end_time').select_related('contest') \ ratings = context['ratings'] = self.object.ratings.order_by('-contest__end_time').select_related('contest') \
@ -142,6 +173,8 @@ class UserAboutPage(UserPage):
'height': '%.3fem' % rating_progress(rating.rating), 'height': '%.3fem' % rating_progress(rating.rating),
} for rating in ratings])) } for rating in ratings]))
context['awards'] = self.get_awards(ratings)
if ratings: if ratings:
user_data = self.object.ratings.aggregate(Min('rating'), Max('rating')) user_data = self.object.ratings.aggregate(Min('rating'), Max('rating'))
global_data = Rating.objects.aggregate(Min('rating'), Max('rating')) global_data = Rating.objects.aggregate(Min('rating'), Max('rating'))
@ -285,7 +318,9 @@ def edit_profile(request):
form.fields['test_site'].initial = request.user.has_perm('judge.test_site') form.fields['test_site'].initial = request.user.has_perm('judge.test_site')
tzmap = settings.TIMEZONE_MAP tzmap = settings.TIMEZONE_MAP
print(settings.REGISTER_NAME_URL)
return render(request, 'user/edit-profile.html', { return render(request, 'user/edit-profile.html', {
'edit_name_url': settings.REGISTER_NAME_URL,
'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA, 'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA,
'form': form, 'title': _('Edit profile'), 'profile': profile, 'form': form, 'title': _('Edit profile'), 'profile': profile,
'has_math_config': bool(settings.MATHOID_URL), 'has_math_config': bool(settings.MATHOID_URL),
@ -367,3 +402,56 @@ class UserLogoutView(TitleMixin, TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
auth_logout(request) auth_logout(request)
return HttpResponseRedirect(request.get_full_path()) return HttpResponseRedirect(request.get_full_path())
class ImportUsersView(TitleMixin, TemplateView):
template_name = 'user/import/index.html'
title = _('Import Users')
def get(self, *args, **kwargs):
if self.request.user.is_superuser:
return super().get(self, *args, **kwargs)
return HttpResponseForbidden()
def import_users_post_file(request):
if not request.user.is_superuser or request.method != 'POST':
return HttpResponseForbidden()
users = import_users.csv_to_dict(request.FILES['csv_file'])
if not users:
return JsonResponse({
'done': False,
'msg': 'No valid row found. Make sure row containing username.'
})
table_html = render_to_string('user/import/table_csv.html', {
'data': users
})
return JsonResponse({
'done': True,
'html': table_html,
'data': users
})
def import_users_submit(request):
import json
if not request.user.is_superuser or request.method != 'POST':
return HttpResponseForbidden()
users = json.loads(request.body)['users']
log = import_users.import_users(users)
return JsonResponse({
'msg': log
})
def sample_import_users(request):
if not request.user.is_superuser or request.method != 'GET':
return HttpResponseForbidden()
filename = 'import_sample.csv'
content = ','.join(import_users.fields) + '\n' + ','.join(import_users.descriptions)
response = HttpResponse(content, content_type='text/plain')
response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
return response

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Arabic, Saudi Arabia\n" "Language-Team: Arabic, Saudi Arabia\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: dmoj\n" "X-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: ar-SA\n" "X-Crowdin-Language: ar-SA\n"
@ -31,4 +32,3 @@ msgstr[5] ""
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-08 21:06-0500\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: German\n" "Language-Team: German\n"

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-21 17:54-0400\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,14 +18,14 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\resources\common.js:203 #: resources/common.js:207
msgctxt "time format with day" msgctxt "time format with day"
msgid "%d day %h:%m:%s" msgid "%d day %h:%m:%s"
msgid_plural "%d days %h:%m:%s" msgid_plural "%d days %h:%m:%s"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: .\resources\common.js:206 #: resources/common.js:210
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:06\n" "PO-Revision-Date: 2019-11-11 22:06\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
@ -27,4 +27,3 @@ msgstr[1] "%d días %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: French\n" "Language-Team: French\n"
@ -27,4 +27,3 @@ msgstr[1] "%d jours %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: dmoj\n" "X-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: hr\n" "X-Crowdin-Language: hr\n"
@ -28,4 +29,3 @@ msgstr[2] "%d dana %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
@ -27,4 +27,3 @@ msgstr[1] "%d nap %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
@ -27,4 +27,3 @@ msgstr[1] ""
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
@ -26,4 +26,3 @@ msgstr[0] "%d 日 %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Korean\n" "Language-Team: Korean\n"
@ -26,4 +26,3 @@ msgstr[0] "%d일 %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Lithuanian\n" "Language-Team: Lithuanian\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && (n%100>19 || n%100<11) ? 0 : (n%10>=2 && n%10<=9) && (n%100>19 || n%100<11) ? 1 : n%1!=0 ? 2: 3);\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && (n%100>19 || n%100<11) ? 0 : (n"
"%10>=2 && n%10<=9) && (n%100>19 || n%100<11) ? 1 : n%1!=0 ? 2: 3);\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: dmoj\n" "X-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: lt\n" "X-Crowdin-Language: lt\n"
@ -29,4 +30,3 @@ msgstr[3] ""
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
@ -27,4 +27,3 @@ msgstr[1] ""
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
@ -10,7 +10,9 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: dmoj\n" "X-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: pl\n" "X-Crowdin-Language: pl\n"
@ -29,4 +31,3 @@ msgstr[3] ""
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
@ -27,4 +27,3 @@ msgstr[1] ""
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "" msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:05\n" "PO-Revision-Date: 2019-11-11 22:05\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Romanian\n" "Language-Team: Romanian\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n"
"%100<20)) ? 1 : 2);\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: dmoj\n" "X-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: ro\n" "X-Crowdin-Language: ro\n"
@ -28,4 +29,3 @@ msgstr[2] "%d zile %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dmoj\n" "Project-Id-Version: dmoj\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 23:12+0000\n" "POT-Creation-Date: 2021-07-20 23:30+0700\n"
"PO-Revision-Date: 2019-11-11 22:06\n" "PO-Revision-Date: 2019-11-11 22:06\n"
"Last-Translator: Icyene\n" "Last-Translator: Icyene\n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
@ -10,7 +10,9 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 "
"&& n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 "
"&& n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: dmoj\n" "X-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: ru\n" "X-Crowdin-Language: ru\n"
@ -29,4 +31,3 @@ msgstr[3] "%d дней %h:%m:%s"
msgctxt "time format without day" msgctxt "time format without day"
msgid "%h:%m:%s" msgid "%h:%m:%s"
msgstr "%h:%m:%s" msgstr "%h:%m:%s"

Some files were not shown because too many files have changed in this diff Show more