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>
node_modules/
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.models import CASCADE
from django.utils.translation import gettext_lazy as _
@ -10,12 +8,23 @@ from judge.models.profile import Profile
__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):
author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
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):
new_message = self.id
@ -27,3 +36,49 @@ class Message(models.Model):
verbose_name = 'message'
verbose_name_plural = 'messages'
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.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.exceptions import PermissionDenied
from django.shortcuts import render
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.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 .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
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):
model = Message
context_object_name = 'message'
template_name = 'chat/chat.html'
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):
return Message.objects.filter(hidden=False)
return self.messages
def get(self, request, *args, **kwargs):
request_room = kwargs['room_id']
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)
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):
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
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5)
context['online_users'] = Profile.objects \
.filter(display_rank='user',
last_access__gte = last_five_minutes)\
.order_by('-rating')
context['admin_status'] = get_admin_online_status()
context['last_msg'] = event.last()
context['status_sections'] = get_status_context(self.request)
context['room'] = self.room_id
context['unread_count_lobby'] = get_unread_count(None, self.request.profile)
if self.room:
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
def delete_message(request):
ret = {'delete': 'done'}
@ -81,17 +101,352 @@ def delete_message(request):
return JsonResponse(ret)
if request.user.is_staff:
messid = int(request.POST.get('messid'))
all_mess = Message.objects.all()
try:
messid = int(request.POST.get('message'))
mess = Message.objects.get(id=messid)
except:
return HttpResponseBadRequest()
for mess in all_mess:
if mess.id == messid:
mess.hidden = True
mess.save()
new_elt = {'time': mess.time, 'content': mess.body}
ret = new_elt
break
mess.hidden = True
mess.save()
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_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
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_PDF_PROBLEM_CACHE = ''
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
@ -125,6 +125,10 @@ SLIMERJS_PAPER_SIZE = 'Letter'
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer'
PUPPETEER_PAPER_SIZE = 'Letter'
USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = 'chromedriver'
PYGMENT_THEME = 'pygment-github.css'
INLINE_JQUERY = True
INLINE_FONTAWESOME = True
@ -239,8 +243,8 @@ INSTALLED_APPS += (
'impersonate',
'django_jinja',
'chat_box',
'channels',
'newsletter',
'django.forms',
)
MIDDLEWARE = (
@ -263,6 +267,8 @@ MIDDLEWARE = (
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
)
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
IMPERSONATE_REQUIRE_SUPERUSER = True
IMPERSONATE_DISABLE_LOGGING = True
@ -484,6 +490,8 @@ SOCIAL_AUTH_PIPELINE = (
'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_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
@ -495,11 +503,6 @@ MOSS_API_KEY = None
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
@ -509,17 +512,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
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
# Amount of seconds to wait between each email. Here 100ms is used.
@ -530,3 +522,12 @@ NEWSLETTER_BATCH_DELAY = 60
# Number of emails in one batch
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.urls import include, url
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, \
ticket, totp, user, widgets
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.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \
ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
from judge.views.select2 import AssigneeSelect2View, ChatUserSearchSelect2View, CommentSelect2View, \
ContestSelect2View, ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
UserSearchSelect2View, UserSelect2View
admin.autodiscover()
@ -115,6 +116,7 @@ urlpatterns = [
url(r'^problem/(?P<problem>[^/]+)', include([
url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'),
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'^/pdf$', 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/init$', problem_init_view, name='problem_data_init'),
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'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'),
@ -225,6 +228,9 @@ urlpatterns = [
url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(),
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]))),
])),
@ -284,6 +290,7 @@ urlpatterns = [
url(r'^select2/', include([
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(),
name='contest_user_search_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'^chat/', include([
url(r'^$',
login_required(ChatView.as_view()),
name='chat'),
url(r'^delete/$', delete_message, name='delete_message')
url(r'^(?P<room_id>\d*)$', login_required(ChatView.as_view()), name='chat'),
url(r'^delete/$', delete_message, name='delete_chat_message'),
url(r'^post/$', post_message, name='post_chat_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/',
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',
@ -389,7 +408,7 @@ favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png',
'favicon-96x96.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',
'apple-touch-icon-120x120.png', 'mstile-310x310.png']
'apple-touch-icon-120x120.png', 'mstile-310x310.png', 'reload.png']
for favicon in favicon_paths:
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.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from django_ace import AceWidget
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
@ -94,7 +96,9 @@ class ContestForm(ModelForm):
class Meta:
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',
attrs={'style': 'width: 100%'}),
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
@ -111,18 +115,19 @@ class ContestForm(ModelForm):
class ContestAdmin(VersionAdmin):
fieldsets = (
(None, {'fields': ('key', 'name', 'organizers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
(None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'scoreboard_visibility',
'run_pretests_only', 'points_precision')}),
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
(_('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')}),
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
'organizations', 'view_contest_scoreboard')}),
(_('Justice'), {'fields': ('banned_users',)}),
)
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
search_fields = ('key', 'name')
inlines = [ContestProblemInline]
actions_on_top = True
actions_on_bottom = True
@ -146,7 +151,7 @@ class ContestAdmin(VersionAdmin):
if request.user.has_perm('judge.edit_all_contest'):
return queryset
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):
readonly = []
@ -158,6 +163,8 @@ class ContestAdmin(VersionAdmin):
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
if not request.user.has_perm('judge.change_contest_visibility'):
readonly += ['is_visible']
if not request.user.has_perm('judge.contest_problem_label'):
readonly += ['problem_label_script']
return readonly
def save_model(self, request, obj, form, change):
@ -185,9 +192,9 @@ class ContestAdmin(VersionAdmin):
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_contest'):
return False
if request.user.has_perm('judge.edit_all_contest') or obj is None:
if obj is None:
return True
return obj.organizers.filter(id=request.profile.id).exists()
return obj.is_editable_by(request.user)
def _rescore(self, contest_key):
from judge.tasks import rescore_contest
@ -232,14 +239,10 @@ class ContestAdmin(VersionAdmin):
if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied()
with transaction.atomic():
if connection.vendor == 'sqlite':
Rating.objects.all().delete()
else:
cursor = connection.cursor()
with connection.cursor() as cursor:
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
cursor.close()
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)
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
@ -247,16 +250,21 @@ class ContestAdmin(VersionAdmin):
if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied()
contest = get_object_or_404(Contest, id=id)
if not contest.is_rated:
if not contest.is_rated or not contest.ended:
raise Http404()
with transaction.atomic():
contest.rate()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
def get_form(self, *args, **kwargs):
form = super(ContestAdmin, self).get_form(*args, **kwargs)
def get_form(self, request, obj=None, **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')
form.base_fields['organizers'].queryset = Profile.objects.filter(
form.base_fields['curators'].queryset = Profile.objects.filter(
Q(user__is_superuser=True) |
Q(user__groups__permissions__codename__in=perms) |
Q(user__user_permissions__codename__in=perms),
@ -274,7 +282,7 @@ class ContestParticipationForm(ModelForm):
class ContestParticipationAdmin(admin.ModelAdmin):
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_on_bottom = actions_on_top = True
search_fields = ('contest__key', 'contest__name', 'user__user__username')
@ -284,7 +292,7 @@ class ContestParticipationAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super(ContestParticipationAdmin, self).get_queryset(request).only(
'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):

View file

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

View file

@ -80,8 +80,10 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
return self.comment_page
def is_comment_locked(self):
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and
not self.request.user.has_perm('judge.override_comment_lock'))
if 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)
def post(self, request, *args, **kwargs):

View file

@ -1,5 +1,6 @@
from judge.contest_format.atcoder import AtCoderContestFormat
from judge.contest_format.default import DefaultContestFormat
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.registry import choices, formats

View file

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

View file

@ -82,6 +82,14 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
"""
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
def best_solution_state(cls, points, total):
if not points:

View file

@ -41,6 +41,7 @@ class DefaultContestFormat(BaseContestFormat):
participation.cumtime = max(cumtime, 0)
participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data
participation.save()
@ -68,3 +69,10 @@ class DefaultContestFormat(BaseContestFormat):
def get_problem_breakdown(self, participation, 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.score = points
participation.tiebreaker = 0
participation.format_data = format_data
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.score = points
participation.tiebreaker = 0
participation.format_data = format_data
participation.save()

View file

@ -27,11 +27,12 @@ else:
def wrap(self, source, outfile):
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:
lexer = pygments.lexers.get_lexer_by_name(language)
except pygments.util.ClassNotFound:
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)))

View file

@ -8,7 +8,7 @@ from statici18n.templatetags.statici18n import inlinei18n
from judge.highlight_code import highlight_code
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)
from . import registry

View file

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

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.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
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.texoid import TEXOID_ENABLED, TexoidRenderer
from .. import registry
@ -26,15 +27,15 @@ class CodeSafeInlineGrammar(mistune.InlineGrammar):
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
class AwesomeInlineGrammar(MathInlineGrammar, CodeSafeInlineGrammar):
class AwesomeInlineGrammar(MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar):
pass
class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer):
class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexer):
grammar_class = AwesomeInlineGrammar
class AwesomeRenderer(MathRenderer, mistune.Renderer):
class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer):
def __init__(self, *args, **kwargs):
self.nofollow = kwargs.pop('nofollow', True)
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,
parse_block_html=1, parse_inline_html=1)
result = markdown(value)
if post_processors:
try:
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))

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.render_with('time-remaining-fragment.html')
def as_countdown(timedelta):
return {'countdown': timedelta}
def as_countdown(time):
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 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):
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('-c', '--chrome', '--puppeteer', action='store_const',
const=PuppeteerPDFRender, dest='engine')
parser.add_argument('-S', '--selenium', action='store_const', const=SeleniumPDFRender, dest='engine')
def handle(self, *args, **options):
try:

View file

@ -6,6 +6,11 @@ from django.db import migrations, models
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):
dependencies = [
@ -13,6 +18,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(create_python3, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='profile',
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:
link = None
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:'):
link = reverse('contest_view', args=(self.page[2:],))
elif self.page.startswith('b:'):

View file

@ -1,12 +1,13 @@
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
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.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
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 judge import contest_format
@ -48,11 +49,25 @@ class ContestTag(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,
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)
organizers = models.ManyToManyField(Profile, help_text=_('These people will be able to edit the contest.'),
related_name='organizers+')
authors = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest.'),
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)
problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem')
start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True)
@ -64,10 +79,9 @@ class Contest(models.Model):
'specified organizations.'))
is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'),
default=False)
hide_scoreboard = models.BooleanField(verbose_name=_('hide scoreboard'),
help_text=_('Whether the scoreboard should remain hidden for the duration '
'of the contest.'),
default=False)
scoreboard_visibility = models.CharField(verbose_name=_('scoreboard visibility'), default=SCOREBOARD_VISIBLE,
max_length=1, help_text=_('Scoreboard visibility through the duration '
'of the contest'), choices=SCOREBOARD_VISIBILITY)
view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True,
related_name='view_contest_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 '
'module. Leave empty to use None. Exact format depends on the contest format '
'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,
validators=[MinValueValidator(0), MaxValueValidator(10)],
help_text=_('Number of digits to round points to.'))
@ -128,30 +146,72 @@ class Contest(models.Model):
def format(self):
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):
# 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:
raise ValidationError('What is this? A contest that ended before it starts?')
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):
if user.is_authenticated:
profile = user.profile
return profile and profile.current_contest is not None and profile.current_contest.contest == self
return False
def can_see_scoreboard(self, user):
if user.has_perm('judge.see_private_contest'):
def can_see_own_scoreboard(self, user):
if self.can_see_full_scoreboard(user):
return True
if user.is_authenticated and self.organizers.filter(id=user.profile.id).exists():
return True
if user.is_authenticated and self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return True
if not self.is_visible:
if not self.can_join:
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
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 True
@ -186,6 +246,19 @@ class Contest(models.Model):
def ended(self):
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):
return self.name
@ -198,50 +271,111 @@ class Contest(models.Model):
update_user_count.alters_data = True
@cached_property
def show_scoreboard(self):
if self.hide_scoreboard and not self.ended:
return False
return True
class Inaccessible(Exception):
pass
class PrivateContest(Exception):
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):
# Contest is publicly visible
if self.is_visible:
# Contest is not private
if not self.is_private and not self.is_organization_private:
return True
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'):
try:
self.access_check(user)
except (self.Inaccessible, self.PrivateContest):
return False
else:
return True
# User can edit the contest
return self.is_editable_by(user)
def is_editable_by(self, user):
# If the user can edit all contests
if user.has_perm('judge.edit_all_contest'):
return True
# If the user is a contest organizer
if user.has_perm('judge.edit_own_contest') and \
self.organizers.filter(id=user.profile.id).exists():
# If the user is a contest organizer or curator
if user.has_perm('judge.edit_own_contest') and user.profile.id in self.editor_ids:
return True
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):
Rating.objects.filter(contest__end_time__gte=self.end_time).delete()
for contest in Contest.objects.filter(is_rated=True, end_time__gte=self.end_time).order_by('end_time'):
Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete()
for contest in Contest.objects.filter(
is_rated=True, end_time__range=(self.end_time, self._now),
).order_by('end_time'):
rate_contest(contest)
class Meta:
@ -255,6 +389,7 @@ class Contest(models.Model):
('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')
@ -271,6 +406,7 @@ class ContestParticipation(models.Model):
cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0)
is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False,
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,
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)
@ -395,7 +531,8 @@ class Rating(models.Model):
related_name='rating', on_delete=CASCADE)
rank = models.IntegerField(verbose_name=_('rank'))
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'))
class Meta:

View file

@ -5,7 +5,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.cache import cache
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
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.functions import Coalesce
from django.urls import reverse
@ -220,6 +220,43 @@ class Problem(models.Model):
def is_subs_manageable_by(self, 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):
return self.name

View file

@ -1,11 +1,13 @@
import errno
import os
from zipfile import BadZipFile, ZipFile
from django.core.validators import FileExtensionValidator
from django.core.cache import cache
from django.db import models
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']
@ -66,7 +68,16 @@ class ProblemData(models.Model):
self.__original_zipfile = self.zipfile
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)
return super(ProblemData, self).save(*args, **kwargs)

View file

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

View file

@ -97,7 +97,10 @@ class Language(models.Model):
@classmethod
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
def get_default_language_pk(cls):

View file

@ -1,3 +1,4 @@
import base64
import errno
import io
import json
@ -10,6 +11,20 @@ import uuid
from django.conf import settings
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_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_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
HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK)
logger = logging.getLogger('judge.problem.pdf')
class BasePdfMaker(object):
math_engine = 'jax'
@ -240,8 +253,8 @@ puppeteer.launch().then(browser => Promise.resolve()
def get_render_script(self):
return self.template.replace('{params}', json.dumps({
'input': 'file://' + os.path.abspath(os.path.join(self.dir, 'input.html')),
'output': os.path.abspath(os.path.join(self.dir, 'output.pdf')),
'input': 'file://%s' % self.htmlfile,
'output': self.pdffile,
'paper': settings.PUPPETEER_PAPER_SIZE,
'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.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:
DefaultPdfMaker = PuppeteerPDFRender
elif HAS_SELENIUM:
DefaultPdfMaker = SeleniumPDFRender
elif HAS_SLIMERJS:
DefaultPdfMaker = SlimerJSPdfMaker
elif HAS_PHANTOMJS:

View file

@ -1,162 +1,197 @@
import math
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.models import Count
from django.db import transaction
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
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):
# 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 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 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 eval_tanhs(tanh_terms, x):
return sum((wt / sd) * tanh((x - mu) / (2 * sd)) for mu, sd, wt in tanh_terms)
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):
# 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 times_rated[i] == 0:
new_volatility[i] = 385
def solve(tanh_terms, y_tg, lin_factor=0, bounds=VALID_RANGE):
L, R = bounds
Ly, Ry = None, None
while R - L > 2:
x = (L + R) / 2
y = lin_factor * x + eval_tanhs(tanh_terms, x)
if y > y_tg:
R, Ry = x, y
elif y < y_tg:
L, Ly = x, y
else:
new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight +
(old_volatility[i] ** 2) / (Weight + 1))
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
return x
# Use linear interpolation to be slightly more accurate.
if Ly is None:
Ly = lin_factor * L + eval_tanhs(tanh_terms, L)
if y_tg <= Ly:
return L
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
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 get_var(times_ranked, cache=[VAR_INIT]):
while times_ranked >= len(cache):
next_var = 1. / (1. / (cache[-1] + VAR_PER_CONTEST) + 1. / BETA2)
cache.append(next_var)
return cache[times_ranked]
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):
from judge.models import Rating, Profile
cursor = connection.cursor()
cursor.execute('''
SELECT judge_rating.user_id, judge_rating.rating, judge_rating.volatility, r.times
FROM judge_rating INNER JOIN
judge_contest ON (judge_contest.id = judge_rating.contest_id) INNER JOIN (
SELECT judge_rating.user_id AS id, MAX(judge_contest.end_time) AS last_time,
COUNT(judge_rating.user_id) AS times
FROM judge_contestparticipation INNER JOIN
judge_rating ON (judge_rating.user_id = judge_contestparticipation.user_id) INNER JOIN
judge_contest ON (judge_contest.id = judge_rating.contest_id)
WHERE judge_contestparticipation.contest_id = %s AND judge_contest.end_time < %s AND
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')
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]), RATING_INIT),
last_mean=Coalesce(Subquery(rating_sorted.values('mean')[:1]), MEAN_INIT),
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',
'last_rating', 'last_mean', 'times')
if not contest.rate_all:
users = users.filter(submissions__gt=0)
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:
users = users.exclude(user__rating__gt=contest.rating_ceiling)
users = list(tie_ranker(users, key=itemgetter(2, 3)))
participation_ids = [user[1][0] for user in users]
user_ids = [user[1][1] for user in users]
ranking = list(map(itemgetter(0), users))
old_data = [data.get(user, (1200, 535, 0)) for user in user_ids]
old_rating = list(map(itemgetter(0), old_data))
old_volatility = list(map(itemgetter(1), old_data))
times_ranked = list(map(itemgetter(2), old_data))
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked)
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))
ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
old_mean = list(map(itemgetter('last_mean'), users))
times_ranked = list(map(itemgetter('times'), users))
historical_p = [[] for _ in users]
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()
ratings = [Rating(user_id=id, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z)
for id, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)]
cursor = connection.cursor()
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)))
ratings = [Rating(user_id=i, contest=contest, rating=r, mean=m, performance=perf,
last_rated=now, participation_id=pid, rank=z)
for i, pid, r, m, perf, z in zip(user_ids, participation_ids, rating, mean, performance, ranking)]
with transaction.atomic():
Rating.objects.filter(contest=contest).delete()
Rating.objects.bulk_create(ratings)
cursor.execute('''
UPDATE `%s` p INNER JOIN `_profile_rating_update` tmp ON (p.id = tmp.id)
SET p.rating = tmp.rating
''' % Profile._meta.db_table)
cursor.execute('DROP TABLE _profile_rating_update')
cursor.close()
return old_rating, old_volatility, ranking, times_ranked, rating, volatility
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]))
RATING_LEVELS = ['Newbie', 'Amateur', 'Expert', 'Candidate Master', 'Master', 'Grandmaster', 'Target']
RATING_VALUES = [1000, 1200, 1500, 1800, 2200, 3000]
RATING_CLASS = ['rate-newbie', 'rate-amateur', 'rate-expert', 'rate-candidate-master',
RATING_VALUES = [1000, 1400, 1700, 1900, 2100, 2400, 3000]
RATING_CLASS = ['rate-newbie', 'rate-amateur', 'rate-specialist', 'rate-expert', 'rate-candidate-master',
'rate-master', 'rate-grandmaster', 'rate-target']

View file

@ -11,7 +11,7 @@ class ProblemSitemap(Sitemap):
priority = 0.8
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):
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 not hasattr(user, 'profile'):
profile = Profile(user=user)
profile.language = Language.get_python3()
profile.language = Language.get_default_language()
logger.info('Info from %s: %s', backend.name, response)
profile.save()
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.contest import *
from judge.tasks.submission import *

View file

@ -1,10 +1,6 @@
from django.contrib.auth.models import User
from django.conf import settings
from judge.models import SubmissionTestCase, Problem, Profile, Language, Organization
from judge.models import SubmissionTestCase, Problem
from collections import defaultdict
import csv
def generate_report(problem):
testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all()
@ -22,51 +18,3 @@ def generate_report(problem):
for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True):
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 os
import re
import shutil
import yaml
import zipfile
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage
from django.urls import reverse
from django.utils.translation import gettext as _
from django.core.cache import cache
VALIDATOR_TEMPLATE_PATH = 'validator_template/template.py'
@ -232,3 +234,61 @@ class ProblemDataCompiler(object):
def generate(cls, *args, **kwargs):
self = cls(*args, **kwargs)
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 math import e
import os, zipfile
from django.conf import settings
from django.core.cache import cache
from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When
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)
qs = cache.get(cache_key)
if qs is None:
qs = Problem.objects.filter(is_public=True, is_organization_private=False,
submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25)
qs = Problem.get_public_problems() \
.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)
if not qs0:

View file

@ -13,22 +13,3 @@ def ranker(iterable, key=attrgetter('points'), rank=0):
yield rank, 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 judge.utils.diggpaginator import DiggPaginator
from django.utils.html import mark_safe
def class_view_decorator(function_decorator):
"""Convert a function based decorator into a class based decorator usable

View file

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

View file

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

View file

@ -57,7 +57,8 @@ class PostList(ListView):
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
context['user_count'] = lazy(Profile.objects.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)
@ -82,16 +83,13 @@ class PostList(ListView):
.order_by('-latest')
[:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT])
visible_contests = Contest.objects.filter(is_visible=True).order_by('start_time')
q = Q(is_private=False, is_organization_private=False)
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) \
.order_by('start_time')
context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now)
context['future_contests'] = visible_contests.filter(start_time__gt=now)
visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True)
if self.request.user.is_authenticated:
profile = self.request.profile
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]
else:
context['open_tickets'] = []
return context

View file

@ -1,4 +1,5 @@
import json
import math
from calendar import Calendar, SUNDAY
from collections import defaultdict, namedtuple
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.exceptions import ImproperlyConfigured, ObjectDoesNotExist
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.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.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.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.timezone import make_aware
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.forms import ContestCloneForm
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.utils.celery import redirect_to_task_status
from judge.utils.opengraph import generate_opengraph
from judge.utils.problems import _get_result_data
from judge.utils.ranker import ranker
from judge.utils.stats import get_bar_chart, get_pie_chart
from judge.utils.views import DiggPaginatorMixin, SingleObjectFormView, TitleMixin, generic_message
from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \
generic_message
from judge.widgets import HeavyPreviewPageDownWidget
__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
'base_contest_ranking_list']
'base_contest_ranking_list', 'ContestClarificationView']
def _find_contest(request, key, private_check=True):
@ -59,29 +62,18 @@ def _find_contest(request, key, private_check=True):
class ContestListMixin(object):
def get_queryset(self):
queryset = Contest.objects.all()
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()
return Contest.get_visible_contests(self.request.user)
class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
model = Contest
paginate_by = 20
template_name = 'contest/list.html'
title = gettext_lazy('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
def _now(self):
@ -101,7 +93,7 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
def _get_queryset(self):
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:
self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip()
@ -114,7 +106,7 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
return queryset
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):
context = super(ContestList, self).get_context_data(**kwargs)
@ -128,12 +120,15 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
if self.request.user.is_authenticated:
for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile,
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:
active.append(participation)
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'))
context['active_participations'] = active
context['current_contests'] = present
@ -143,9 +138,8 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
context['contest_query'] = self.contest_query
context['org_query'] = self.org_query
context['organizations'] = Organization.objects.all()
context['page_suffix'] = suffix = (
'?' + self.request.GET.urlencode()) if self.request.GET else ''
context['first_page_href'] = (self.first_page_href or '.') + suffix
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context
@ -164,37 +158,44 @@ class ContestMixin(object):
slug_url_kwarg = 'contest'
@cached_property
def is_organizer(self):
return self.check_organizer()
def is_editor(self):
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):
if user is None:
user = self.request.user
return (contest or self.object).is_editable_by(user)
@cached_property
def is_tester(self):
if not self.request.user.is_authenticated:
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):
context = super(ContestMixin, self).get_context_data(**kwargs)
if self.request.user.is_authenticated:
profile = self.request.profile
in_contest = context['in_contest'] = (profile.current_contest is not None and
profile.current_contest.contest == self.object)
if in_contest:
context['participation'] = profile.current_contest
context['participating'] = True
try:
context['live_participation'] = (
self.request.profile.contest_history.get(
contest=self.object,
virtual=ContestParticipation.LIVE,
)
)
except ContestParticipation.DoesNotExist:
context['live_participation'] = None
context['has_joined'] = False
else:
try:
context['participation'] = profile.contest_history.get(contest=self.object, virtual=0)
except ContestParticipation.DoesNotExist:
context['participating'] = False
context['participation'] = None
else:
context['participating'] = True
context['has_joined'] = True
else:
context['participating'] = False
context['participation'] = None
context['in_contest'] = False
context['live_participation'] = None
context['has_joined'] = False
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:
metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id,
@ -210,17 +211,21 @@ class ContestMixin(object):
def get_object(self, queryset=None):
contest = super(ContestMixin, self).get_object(queryset)
user = self.request.user
profile = self.request.profile
if (profile is not None and
ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()):
return contest
if not contest.is_visible and not user.has_perm('judge.see_private_contest') and (
not user.has_perm('judge.edit_own_contest') or
not self.check_organizer(contest, user)):
try:
contest.access_check(self.request.user)
except Contest.PrivateContest:
raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private,
contest.organizations.all())
except Contest.Inaccessible:
raise Http404()
else:
return contest
if contest.is_private or contest.is_organization_private:
private_contest_error = PrivateContestError(contest.name, contest.is_private,
@ -297,7 +302,7 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje
contest.organizations.set(organizations)
contest.private_contestants.set(private_contestants)
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:
problem.contest = contest
@ -337,7 +342,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
def join_contest(self, request, access_code=None):
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'),
_('"%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 are permanently barred from joining this contest.'))
requires_access_code = (not (request.user.is_superuser or self.is_organizer) and
contest.access_code and access_code != contest.access_code)
requires_access_code = (not self.can_edit and contest.access_code and access_code != contest.access_code)
if contest.ended:
if requires_access_code:
raise ContestAccessDenied()
@ -371,22 +375,24 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
else:
break
else:
SPECTATE = ContestParticipation.SPECTATE
LIVE = ContestParticipation.LIVE
try:
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:
if requires_access_code:
raise ContestAccessDenied()
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(),
)
else:
if participation.ended:
participation = ContestParticipation.objects.get_or_create(
contest=contest, user=profile, virtual=-1,
contest=contest, user=profile, virtual=SPECTATE,
defaults={'real_start': timezone.now()},
)[0]
@ -449,7 +455,7 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
def get_contest_data(self, start, end):
end += timedelta(days=1)
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))
for contest in contests:
start_date = timezone.localtime(contest.start_time).date()
@ -523,6 +529,7 @@ class CachedContestCalendar(ContestCalendar):
class ContestStats(TitleMixin, ContestMixin, DetailView):
template_name = 'contest/stats.html'
POINT_BIN = 10 # in point distribution
def get_title(self):
return _('%s Statistics') % self.object.name
@ -530,7 +537,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
def get_context_data(self, **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()
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'))
.values_list('problem__code', 'result', 'count'),
)
labels, codes = zip(
*self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code'),
)
labels, codes = [], []
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)
status_counts = [[] for i in range(num_problems)]
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']:
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 = {
'problem_status_count': {
'labels': labels,
@ -572,6 +595,9 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=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(
queryset.values('language__name').annotate(count=Count('language__name'))
.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['problems'] = labels
return context
ContestRankingProfile = namedtuple(
'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',
)
@ -605,6 +631,7 @@ def make_contest_ranking_profile(contest, participation, contest_problems):
username=user.username,
points=participation.score,
cumtime=participation.cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None,
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')]
def contest_ranking_list(contest, problems):
return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0, user__is_unlisted=False)
def contest_ranking_list(contest, problems, queryset=None):
if not queryset:
queryset = contest.users.filter(virtual=0)
return base_contest_ranking_list(contest, problems, queryset
.prefetch_related('user__organizations')
.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,
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'))
if contest.hide_scoreboard and contest.is_in_contest(request.user):
return ([(_('???'), make_contest_ranking_profile(contest, request.profile.current_contest, problems))],
problems)
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime'))
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker'))
if show_current_virtual:
if participation is None and request.user.is_authenticated:
@ -651,15 +676,24 @@ def contest_ranking_ajax(request, contest, participation=None):
if not exists:
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()
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', {
'users': users,
'problems': problems,
'contest': contest,
'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):
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()
users, problems = self.get_ranking_list()
context['users'] = users
context['problems'] = problems
context['last_msg'] = event.last()
context['tab'] = self.tab
return context
@ -697,6 +730,14 @@ class ContestRanking(ContestRankingBase):
return _('%s Rankings') % self.object.name
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)
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)
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')
live_link = format_html('<a href="{2}#!{1}">{0}</a>', _('Live'), self.profile.username,
reverse('contest_ranking', args=[self.object.key]))
@ -728,6 +772,7 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
context['has_rating'] = False
context['now'] = timezone.now()
context['rank_header'] = _('Participation')
context['participation_tab'] = True
return context
def get(self, request, *args, **kwargs):
@ -762,7 +807,7 @@ class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
def get_object(self, queryset=None):
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()
if not contest.is_editable_by(self.request.user):
raise Http404()
@ -824,3 +869,87 @@ class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
def get_title(self):
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.urls import reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy, ungettext
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
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)
.values_list('page').annotate(count=Count('page')).order_by()
}
context['pending_count'] = OrganizationRequest.objects.filter(state='P', organization=self.object).count()
return context
@ -230,7 +232,7 @@ class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView):
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
slug_field = 'key'
slug_url_kwarg = 'key'
@ -243,6 +245,10 @@ class OrganizationRequestBaseView(LoginRequiredMixin, SingleObjectTemplateRespon
raise PermissionDenied()
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):
context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs)
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.safestring import mark_safe
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.detail import SingleObjectMixin
from judge.comments import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemGroup, \
ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \
ProblemGroup, ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
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'
template_name = 'problem/problem.html'
def get_comment_page(self):
return 'p:%s' % self.object.code
def get_context_data(self, **kwargs):
context = super(ProblemDetail, self).get_context_data(**kwargs)
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
get_contest_problem(self.object, user.profile))
context['contest_problem'] = contest_problem
if contest_problem:
clarifications = self.object.clarifications
context['has_clarifications'] = clarifications.count() > 0
@ -220,6 +218,21 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
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):
pass
@ -256,7 +269,6 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
'math_engine': maker.math_engine,
}).replace('"//', '"https://').replace("'//", "'https://")
maker.title = problem_name
assets = ['style.css', 'pygment-github.css']
if maker.math_engine == 'jax':
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)
return HttpResponse(maker.log, status=500, content_type='text/plain')
shutil.move(maker.pdffile, cache)
response = HttpResponse()
if hasattr(settings, 'DMOJ_PDF_PROBLEM_INTERNAL') and \
request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'):
@ -438,7 +449,17 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
else:
context['hot_problems'] = None
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
def get_noui_slider_points(self):

View file

@ -2,14 +2,24 @@ import json
import mimetypes
import os
from itertools import chain
import shutil
from tempfile import gettempdir
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.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.files import File
from django.core.exceptions import ValidationError
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.urls import reverse
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.unicode import utf8text
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
mimetypes.init()
@ -52,6 +63,7 @@ class ProblemDataForm(ModelForm):
model = ProblemData
fields = ['zipfile', 'checker', 'checker_args', 'custom_checker', 'custom_validator']
widgets = {
'zipfile': FineUploadFileInput,
'checker_args': HiddenInput,
'generator': HiddenInput,
'output_limit': HiddenInput,
@ -76,6 +88,7 @@ class ProblemCaseForm(ModelForm):
}
class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1,
can_delete=True)):
model = ProblemTestCase
@ -242,3 +255,39 @@ def problem_init_view(request, problem):
format_html('<a href="{1}">{0}</a>', problem.name,
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:
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):
if User.objects.filter(email=self.cleaned_data['email']).exists():
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):
user = super(RegistrationView, self).register(form)
profile, _ = Profile.objects.get_or_create(user=user, defaults={
'language': Language.get_python3(),
'language': Language.get_default_language(),
})
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.views.generic.list import BaseListView
from chat_box.utils import encrypt_url
from judge.jinja2.gravatar import gravatar
from judge.models import Comment, Contest, Organization, Problem, Profile
@ -54,29 +56,14 @@ class OrganizationSelect2View(Select2View):
class ProblemSelect2View(Select2View):
def get_queryset(self):
queryset = Problem.objects.filter(Q(code__icontains=self.term) | Q(name__icontains=self.term))
if not self.request.user.has_perm('judge.see_private_problem'):
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()
return Problem.get_visible_problems(self.request.user) \
.filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)).distinct()
class ContestSelect2View(Select2View):
def get_queryset(self):
queryset = Contest.objects.filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
if not self.request.user.has_perm('judge.see_private_contest'):
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
return Contest.get_visible_contests(self.request.user) \
.filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
class CommentSelect2View(Select2View):
@ -119,8 +106,7 @@ class UserSearchSelect2View(BaseListView):
class ContestUserSearchSelect2View(UserSearchSelect2View):
def get_queryset(self):
contest = get_object_or_404(Contest, key=self.kwargs['contest'])
if not contest.can_see_scoreboard(self.request.user) or \
contest.hide_scoreboard and contest.is_in_contest(self.request.user):
if not contest.is_accessible_by(self.request.user) or not contest.can_see_full_scoreboard(self.request.user):
raise Http404()
return Profile.objects.filter(contest_history__contest=contest,
@ -137,3 +123,35 @@ class AssigneeSelect2View(UserSearchSelect2View):
def get_queryset(self):
return Profile.objects.filter(assigned_tickets__isnull=False,
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_completed_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 TitleMixin
@ -110,7 +111,7 @@ class SubmissionSource(SubmissionDetailBase):
submission = self.object
context['raw_source'] = submission.source.source.rstrip('\n')
context['highlighted_source'] = highlight_code(
submission.source.source, submission.language.pygments)
submission.source.source, submission.language.pygments, linenos=False)
return context
@ -137,60 +138,28 @@ def group_test_cases(cases):
return result
def read_head_archive(archive, 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)
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)
def get_cases_data(submission):
testcases = ProblemTestCase.objects.filter(dataset=submission.problem)\
.order_by('order')
if (submission.is_pretested):
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 = {}
for count, case in enumerate(testcases):
problem_data[count + 1] = get_input_answer(case, archive)
count = 0
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
@ -198,20 +167,36 @@ def get_problem_data(submission):
class SubmissionStatus(SubmissionDetailBase):
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):
context = super(SubmissionStatus, self).get_context_data(**kwargs)
submission = self.object
context['last_msg'] = event.last()
context['batches'] = group_test_cases(submission.test_cases.all())
context['time_limit'] = submission.problem.time_limit
context['can_see_testcases'] = False
contest = submission.contest_or_none
prefix_length = 0
can_see_testcases = self.access_testcases_in_contest()
if (contest is not None):
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:
lang_limit = submission.problem.language_limits.get(
language=submission.language)
@ -292,11 +277,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
queryset=ProblemTranslation.objects.filter(
language=self.request.LANGUAGE_CODE), to_attr='_trans'))
if self.in_contest:
queryset = queryset.filter(
contest__participation__contest_id=self.contest.id)
if self.contest.hide_scoreboard and self.contest.is_in_contest(self.request.user):
queryset = queryset.filter(
contest__participation__user=self.request.profile)
queryset = queryset.filter(contest_object=self.contest)
if not self.contest.can_see_full_scoreboard(self.request.user):
queryset = queryset.filter(user=self.request.profile)
else:
queryset = queryset.select_related(
'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
# the join would be far too messy
if not self.request.user.has_perm('judge.see_private_contest'):
queryset = queryset.exclude(
contest_object_id__in=Contest.objects.filter(hide_scoreboard=True))
# Show submissions for any contest you can edit or visible scoreboard
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:
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:
queryset = queryset.filter(result__in=self.selected_statuses)
@ -318,14 +307,13 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
def get_queryset(self):
queryset = self._get_queryset()
if not self.in_contest:
if not self.request.user.has_perm('judge.see_private_problem'):
queryset = queryset.filter(problem__is_public=True)
if not self.request.user.has_perm('judge.see_organization_problem'):
filter = Q(problem__is_organization_private=False)
if self.request.user.is_authenticated:
filter |= Q(
problem__organizations__in=self.request.profile.organizations.all())
queryset = queryset.filter(filter)
join_sql_subquery(
queryset,
subquery=str(Problem.get_visible_problems(self.request.user).distinct().only('id').query),
params=[],
join_fields=[('problem_id', 'id')],
alias='visible_problems',
)
return queryset
def get_my_submissions_page(self):
@ -452,7 +440,7 @@ class ProblemSubmissionsBase(SubmissionsListBase):
reverse('problem_detail', args=[self.problem.code]))
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()
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.fields import DateField
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.urls import reverse
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.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
from django.views import View
from django.views.generic import DetailView, ListView, TemplateView
from django.template.loader import render_to_string
from reversion import revisions
from judge.forms import ProfileForm, newsletter_id
from judge.models import Profile, Rating, Submission, Friend
from judge.performance_points import get_pp_breakdown
from judge.ratings import rating_class, rating_progress
from judge.tasks import import_users
from judge.utils.problems import contest_completed_ids, user_completed_ids
from judge.utils.ranker import ranker
from judge.utils.subscription import Subscription
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
__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
_('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?
@cached_property
def profile(self):
@ -126,6 +135,28 @@ EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
class UserAboutPage(UserPage):
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):
context = super(UserAboutPage, self).get_context_data(**kwargs)
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),
} for rating in ratings]))
context['awards'] = self.get_awards(ratings)
if ratings:
user_data = self.object.ratings.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')
tzmap = settings.TIMEZONE_MAP
print(settings.REGISTER_NAME_URL)
return render(request, 'user/edit-profile.html', {
'edit_name_url': settings.REGISTER_NAME_URL,
'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA,
'form': form, 'title': _('Edit profile'), 'profile': profile,
'has_math_config': bool(settings.MATHOID_URL),
@ -367,3 +402,56 @@ class UserLogoutView(TitleMixin, TemplateView):
def post(self, request, *args, **kwargs):
auth_logout(request)
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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Arabic, Saudi Arabia\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\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-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: ar-SA\n"
@ -31,4 +32,3 @@ msgstr[5] ""
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: German\n"

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,14 +18,14 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\resources\common.js:203
#: resources/common.js:207
msgctxt "time format with day"
msgid "%d day %h:%m:%s"
msgid_plural "%d days %h:%m:%s"
msgstr[0] ""
msgstr[1] ""
#: .\resources\common.js:206
#: resources/common.js:210
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Spanish\n"
@ -27,4 +27,3 @@ msgstr[1] "%d días %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: French\n"
@ -27,4 +27,3 @@ msgstr[1] "%d jours %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Croatian\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\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-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: hr\n"
@ -28,4 +29,3 @@ msgstr[2] "%d dana %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Hungarian\n"
@ -27,4 +27,3 @@ msgstr[1] "%d nap %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Italian\n"
@ -27,4 +27,3 @@ msgstr[1] ""
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Japanese\n"
@ -26,4 +26,3 @@ msgstr[0] "%d 日 %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Korean\n"
@ -26,4 +26,3 @@ msgstr[0] "%d일 %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Lithuanian\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\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-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: lt\n"
@ -29,4 +30,3 @@ msgstr[3] ""
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Dutch\n"
@ -27,4 +27,3 @@ msgstr[1] ""
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Polish\n"
@ -10,7 +10,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\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-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: pl\n"
@ -29,4 +31,3 @@ msgstr[3] ""
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Portuguese, Brazilian\n"
@ -27,4 +27,3 @@ msgstr[1] ""
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Romanian\n"
@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\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-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: ro\n"
@ -28,4 +29,3 @@ msgstr[2] "%d zile %h:%m:%s"
msgctxt "time format without day"
msgid "%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 ""
"Project-Id-Version: dmoj\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"
"Last-Translator: Icyene\n"
"Language-Team: Russian\n"
@ -10,7 +10,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\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-Crowdin-Project: dmoj\n"
"X-Crowdin-Language: ru\n"
@ -29,4 +31,3 @@ msgstr[3] "%d дней %h:%m:%s"
msgctxt "time format without day"
msgid "%h:%m:%s"
msgstr "%h:%m:%s"

File diff suppressed because it is too large Load diff

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