diff --git a/chat_box/apps.py b/chat_box/apps.py index fbc3ca1..dd90bc1 100644 --- a/chat_box/apps.py +++ b/chat_box/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class ChatBoxConfig(AppConfig): - name = 'chat_box' + name = "chat_box" diff --git a/chat_box/migrations/0001_initial.py b/chat_box/migrations/0001_initial.py index b311ab5..f738c77 100644 --- a/chat_box/migrations/0001_initial.py +++ b/chat_box/migrations/0001_initial.py @@ -9,22 +9,43 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('judge', '0100_auto_20200127_0059'), + ("judge", "0100_auto_20200127_0059"), ] operations = [ migrations.CreateModel( - name='Message', + name="Message", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')), - ('body', models.TextField(max_length=8192, verbose_name='body of comment')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField(auto_now_add=True, verbose_name="posted time"), + ), + ( + "body", + models.TextField(max_length=8192, verbose_name="body of comment"), + ), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Profile", + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'message', - 'verbose_name_plural': 'messages', - 'ordering': ('-time',), + "verbose_name": "message", + "verbose_name_plural": "messages", + "ordering": ("-time",), }, ), ] diff --git a/chat_box/migrations/0002_message_hidden.py b/chat_box/migrations/0002_message_hidden.py index 23c02da..baef87e 100644 --- a/chat_box/migrations/0002_message_hidden.py +++ b/chat_box/migrations/0002_message_hidden.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('chat_box', '0001_initial'), + ("chat_box", "0001_initial"), ] operations = [ migrations.AddField( - model_name='message', - name='hidden', - field=models.BooleanField(default=False, verbose_name='is hidden'), + model_name="message", + name="hidden", + field=models.BooleanField(default=False, verbose_name="is hidden"), ), ] diff --git a/chat_box/migrations/0003_auto_20200505_2306.py b/chat_box/migrations/0003_auto_20200505_2306.py index 28efdb9..7af087e 100644 --- a/chat_box/migrations/0003_auto_20200505_2306.py +++ b/chat_box/migrations/0003_auto_20200505_2306.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('chat_box', '0002_message_hidden'), + ("chat_box", "0002_message_hidden"), ] operations = [ migrations.AlterField( - model_name='message', - name='hidden', - field=models.BooleanField(default=True, verbose_name='is hidden'), + model_name="message", + name="hidden", + field=models.BooleanField(default=True, verbose_name="is hidden"), ), ] diff --git a/chat_box/migrations/0004_auto_20200505_2336.py b/chat_box/migrations/0004_auto_20200505_2336.py index 3ecc44c..7475262 100644 --- a/chat_box/migrations/0004_auto_20200505_2336.py +++ b/chat_box/migrations/0004_auto_20200505_2336.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('chat_box', '0003_auto_20200505_2306'), + ("chat_box", "0003_auto_20200505_2306"), ] operations = [ migrations.AlterField( - model_name='message', - name='hidden', - field=models.BooleanField(default=False, verbose_name='is hidden'), + model_name="message", + name="hidden", + field=models.BooleanField(default=False, verbose_name="is hidden"), ), ] diff --git a/chat_box/migrations/0005_auto_20211011_0714.py b/chat_box/migrations/0005_auto_20211011_0714.py index eaf26cb..a28957e 100644 --- a/chat_box/migrations/0005_auto_20211011_0714.py +++ b/chat_box/migrations/0005_auto_20211011_0714.py @@ -7,22 +7,52 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0116_auto_20211011_0645'), - ('chat_box', '0004_auto_20200505_2336'), + ("judge", "0116_auto_20211011_0645"), + ("chat_box", "0004_auto_20200505_2336"), ] operations = [ migrations.CreateModel( - name='Room', + 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')), + ( + "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'), + 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", + ), ), ] diff --git a/chat_box/migrations/0006_userroom.py b/chat_box/migrations/0006_userroom.py index eff219c..534e1a3 100644 --- a/chat_box/migrations/0006_userroom.py +++ b/chat_box/migrations/0006_userroom.py @@ -7,18 +7,42 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0116_auto_20211011_0645'), - ('chat_box', '0005_auto_20211011_0714'), + ("judge", "0116_auto_20211011_0645"), + ("chat_box", "0005_auto_20211011_0714"), ] operations = [ migrations.CreateModel( - name='UserRoom', + 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')), + ( + "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", + ), + ), ], ), ] diff --git a/chat_box/migrations/0007_auto_20211112_1255.py b/chat_box/migrations/0007_auto_20211112_1255.py index 49a39f0..c38e475 100644 --- a/chat_box/migrations/0007_auto_20211112_1255.py +++ b/chat_box/migrations/0007_auto_20211112_1255.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('chat_box', '0006_userroom'), + ("chat_box", "0006_userroom"), ] operations = [ migrations.AlterField( - model_name='userroom', - name='last_seen', - field=models.DateTimeField(auto_now_add=True, verbose_name='last seen'), + model_name="userroom", + name="last_seen", + field=models.DateTimeField(auto_now_add=True, verbose_name="last seen"), ), ] diff --git a/chat_box/migrations/0008_ignore.py b/chat_box/migrations/0008_ignore.py index 723dd3b..6648d1e 100644 --- a/chat_box/migrations/0008_ignore.py +++ b/chat_box/migrations/0008_ignore.py @@ -7,17 +7,33 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0116_auto_20211011_0645'), - ('chat_box', '0007_auto_20211112_1255'), + ("judge", "0116_auto_20211011_0645"), + ("chat_box", "0007_auto_20211112_1255"), ] operations = [ migrations.CreateModel( - name='Ignore', + 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')), + ( + "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", + ), + ), ], ), ] diff --git a/chat_box/models.py b/chat_box/models.py index 1a62171..132992a 100644 --- a/chat_box/models.py +++ b/chat_box/models.py @@ -6,25 +6,35 @@ from django.utils.translation import gettext_lazy as _ from judge.models.profile import Profile -__all__ = ['Message'] +__all__ = ["Message"] + class Room(models.Model): - user_one = models.ForeignKey(Profile, related_name="user_one", verbose_name='user 1', on_delete=CASCADE) - user_two = models.ForeignKey(Profile, related_name="user_two", verbose_name='user 2', on_delete=CASCADE) + 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) + 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 @@ -32,26 +42,37 @@ class Message(models.Model): super(Message, self).save(*args, **kwargs) class Meta: - app_label = 'chat_box' - verbose_name = 'message' - verbose_name_plural = 'messages' - ordering = ('-time',) + app_label = "chat_box" + 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) + 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) + 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() + return ( + current_user.ignored_chat_users.get() + .ignored_users.filter(id=new_friend.id) + .exists() + ) except: return False @@ -64,21 +85,17 @@ class Ignore(models.Model): @classmethod def add_ignore(self, current_user, friend): - ignore, created = self.objects.get_or_create( - user = current_user - ) + 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, 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)): + if self.is_ignored(current_user, friend): self.remove_ignore(current_user, friend) else: - self.add_ignore(current_user, friend) \ No newline at end of file + self.add_ignore(current_user, friend) diff --git a/chat_box/utils.py b/chat_box/utils.py index 5ebb16e..ad93ad4 100644 --- a/chat_box/utils.py +++ b/chat_box/utils.py @@ -5,14 +5,16 @@ 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) + 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('_') + creator_id, other_id = dec_message.split("_") return int(creator_id), int(other_id) except Exception as e: - return None, None \ No newline at end of file + return None, None diff --git a/chat_box/views.py b/chat_box/views.py index deffafa..1d303ef 100644 --- a/chat_box/views.py +++ b/chat_box/views.py @@ -1,11 +1,27 @@ from django.utils.translation import gettext as _ from django.views.generic import ListView -from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect, HttpResponseRedirect +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 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 @@ -24,10 +40,10 @@ import json class ChatView(ListView): - context_object_name = 'message' - template_name = 'chat/chat.html' - title = _('Chat Box') - + context_object_name = "message" + template_name = "chat/chat.html" + title = _("Chat Box") + def __init__(self): super().__init__() self.room_id = None @@ -40,8 +56,8 @@ class ChatView(ListView): return self.messages def get(self, request, *args, **kwargs): - request_room = kwargs['room_id'] - page = request.GET.get('page') + request_room = kwargs["room_id"] + page = request.GET.get("page") if request_room: try: @@ -64,109 +80,121 @@ class ChatView(ListView): cur_page = self.paginator.get_page(page) - return render(request, 'chat/message_list.html', { - 'object_list': cur_page.object_list, - 'num_pages': self.paginator.num_pages - }) + 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) - context['title'] = self.title - 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) + context["title"] = self.title + 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']) + 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' - } + 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'} - - if request.method == 'GET': + ret = {"delete": "done"} + + if request.method == "GET": return JsonResponse(ret) if request.user.is_staff: try: - messid = int(request.POST.get('message')) + messid = int(request.POST.get("message")) mess = Message.objects.get(id=messid) except: return HttpResponseBadRequest() - + mess.hidden = True mess.save() - + return JsonResponse(ret) - + return JsonResponse(ret) @login_required def post_message(request): - ret = {'msg': 'posted'} - if request.method != 'POST': + ret = {"msg": "posted"} + if request.method != "POST": return HttpResponseBadRequest() - if len(request.POST['body']) > 5000: + if len(request.POST["body"]) > 5000: return HttpResponseBadRequest() room = None - if request.POST['room']: - room = Room.objects.get(id=request.POST['room']) - + 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 = 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') - }) + 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') - }) - + 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 + 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': + if request.method != "GET": return HttpResponseBadRequest() try: - message_id = request.GET['message'] + message_id = request.GET["message"] except KeyError: return HttpResponseBadRequest() @@ -174,22 +202,26 @@ def chat_message_ajax(request): 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) + return HttpResponse("Unauthorized", status=401) except Message.DoesNotExist: return HttpResponseBadRequest() - return render(request, 'chat/message.html', { - 'message': message, - }) + 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') + 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() @@ -210,11 +242,11 @@ def update_last_seen(request, **kwargs): user_room.last_seen = timezone.now() user_room.save() - return JsonResponse({'msg': 'updated'}) + return JsonResponse({"msg": "updated"}) def get_online_count(): - last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) + last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) return Profile.objects.filter(last_access__gte=last_two_minutes).count() @@ -225,10 +257,10 @@ def get_user_online_status(user): def user_online_status_ajax(request): - if request.method != 'GET': + if request.method != "GET": return HttpResponseBadRequest() - user_id = request.GET.get('user') + user_id = request.GET.get("user") if user_id: try: @@ -238,37 +270,45 @@ def user_online_status_ajax(request): 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) - }) + 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(), - }) + 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) + 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'] + count[i["other_user"]] = i["unread_count"] for user in queryset: is_online = False - if (user.last_access >= last_two_minutes): + if user.last_access >= last_two_minutes: is_online = True - user_dict = {'user': user, 'is_online': is_online} + 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) + user_dict["unread_count"] = count[user.id] + user_dict["url"] = encrypt_url(request_user.id, user.id) ret.append(user_dict) return ret @@ -281,66 +321,82 @@ def get_status_context(request, include_ignored=False): 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_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] + 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] + 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_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)\ + 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": "Recent", + "user_list": get_online_status(request.profile, recent_list, recent_rooms), }, { - 'title': 'Following', - 'user_list': get_online_status(request.profile, friend_list), + "title": "Following", + "user_list": get_online_status(request.profile, friend_list), }, { - 'title': 'Admin', - 'user_list': get_online_status(request.profile, admin_list), + "title": "Admin", + "user_list": get_online_status(request.profile, admin_list), }, { - 'title': 'Other', - 'user_list': get_online_status(request.profile, all_user_status), + "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), - }) + return render( + request, + "chat/online_status.html", + { + "status_sections": get_status_context(request), + "unread_count_lobby": get_unread_count(None, request.profile), + }, + ) @login_required @@ -353,10 +409,10 @@ def get_room(user_one, user_two): @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') + 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() @@ -371,7 +427,7 @@ def get_or_create_room(request): return HttpResponseBadRequest() user = request.profile - + if not other_user or not user: return HttpResponseBadRequest() # TODO: each user can only create <= 300 rooms @@ -382,47 +438,60 @@ def get_or_create_room(request): 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})) + 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') + 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)\ + 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') + 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)\ + 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) + ) + .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'] + user_id = kwargs["user_id"] if not user_id: return HttpResponseBadRequest() try: @@ -431,28 +500,34 @@ def toggle_ignore(request, **kwargs): return HttpResponseBadRequest() Ignore.toggle_ignore(request.profile, other_user) - next_url = request.GET.get('next', '/') + next_url = request.GET.get("next", "/") return HttpResponseRedirect(next_url) @login_required def get_unread_boxes(request): - if (request.method != 'GET'): + if request.method != "GET": return HttpResponseBadRequest() ignored_users = Ignore.get_ignored_users(request.profile) - mess = Message.objects.filter(room=OuterRef('room'), - time__gte=OuterRef('last_seen'))\ - .exclude(author=request.profile)\ - .exclude(author__in=ignored_users)\ - .order_by().values('room')\ - .annotate(unread_count=Count('pk')).values('unread_count') + mess = ( + Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen")) + .exclude(author=request.profile) + .exclude(author__in=ignored_users) + .order_by() + .values("room") + .annotate(unread_count=Count("pk")) + .values("unread_count") + ) - unread_boxes = UserRoom.objects\ - .filter(user=request.profile, room__isnull=False)\ + 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() + ) + .filter(unread_count__gte=1) + .count() + ) - return JsonResponse({'unread_boxes': unread_boxes}) \ No newline at end of file + return JsonResponse({"unread_boxes": unread_boxes}) diff --git a/django_2_2_pymysql_patch.py b/django_2_2_pymysql_patch.py index 4470180..c39c823 100644 --- a/django_2_2_pymysql_patch.py +++ b/django_2_2_pymysql_patch.py @@ -12,6 +12,6 @@ if (2, 2) <= django.VERSION < (3,): # attribute where the exact query sent to the database is saved. # See MySQLdb/cursors.py in the source distribution. # MySQLdb returns string, PyMySQL bytes. - return force_text(getattr(cursor, '_executed', None), errors='replace') + return force_text(getattr(cursor, "_executed", None), errors="replace") DatabaseOperations.last_executed_query = last_executed_query diff --git a/django_ace/widgets.py b/django_ace/widgets.py index 91369d0..38fcd75 100644 --- a/django_ace/widgets.py +++ b/django_ace/widgets.py @@ -11,8 +11,17 @@ from django.utils.safestring import mark_safe class AceWidget(forms.Textarea): - def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px', - no_ace_media=False, *args, **kwargs): + def __init__( + self, + mode=None, + theme=None, + wordwrap=False, + width="100%", + height="300px", + no_ace_media=False, + *args, + **kwargs + ): self.mode = mode self.theme = theme self.wordwrap = wordwrap @@ -23,10 +32,10 @@ class AceWidget(forms.Textarea): @property def media(self): - js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else [] - js.append('django_ace/widget.js') + js = [urljoin(settings.ACE_URL, "ace.js")] if self.ace_media else [] + js.append("django_ace/widget.js") css = { - 'screen': ['django_ace/widget.css'], + "screen": ["django_ace/widget.css"], } return forms.Media(js=js, css=css) @@ -34,24 +43,28 @@ class AceWidget(forms.Textarea): attrs = attrs or {} ace_attrs = { - 'class': 'django-ace-widget loading', - 'style': 'width:%s; height:%s' % (self.width, self.height), - 'id': 'ace_%s' % name, + "class": "django-ace-widget loading", + "style": "width:%s; height:%s" % (self.width, self.height), + "id": "ace_%s" % name, } if self.mode: - ace_attrs['data-mode'] = self.mode + ace_attrs["data-mode"] = self.mode if self.theme: - ace_attrs['data-theme'] = self.theme + ace_attrs["data-theme"] = self.theme if self.wordwrap: - ace_attrs['data-wordwrap'] = 'true' + ace_attrs["data-wordwrap"] = "true" - attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none') + attrs.update( + style="width: 100%; min-width: 100%; max-width: 100%; resize: none" + ) textarea = super(AceWidget, self).render(name, value, attrs) - html = '
%s' % (flatatt(ace_attrs), textarea) + html = "
%s" % (flatatt(ace_attrs), textarea) # add toolbar - html = ('
' - '
%s
') % html + html = ( + '
' + '
%s
' + ) % html return mark_safe(html) diff --git a/dmoj/celery.py b/dmoj/celery.py index 96718ea..5f0c87d 100644 --- a/dmoj/celery.py +++ b/dmoj/celery.py @@ -4,24 +4,30 @@ import socket from celery import Celery from celery.signals import task_failure -app = Celery('dmoj') +app = Celery("dmoj") from django.conf import settings # noqa: E402, I202, django must be imported here -app.config_from_object(settings, namespace='CELERY') -if hasattr(settings, 'CELERY_BROKER_URL_SECRET'): +app.config_from_object(settings, namespace="CELERY") + +if hasattr(settings, "CELERY_BROKER_URL_SECRET"): app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET -if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'): +if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"): app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET # Load task modules from all registered Django app configs. app.autodiscover_tasks() # Logger to enable errors be reported. -logger = logging.getLogger('judge.celery') +logger = logging.getLogger("judge.celery") @task_failure.connect() def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs): - logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201 - exc_info=(type(exception), exception, traceback)) + logger.error( + "Celery Task %s: %s on %s", + sender.name, + task_id, + socket.gethostname(), # noqa: G201 + exc_info=(type(exception), exception, traceback), + ) diff --git a/dmoj/decorators.py b/dmoj/decorators.py index b6c6d0d..fe483c5 100644 --- a/dmoj/decorators.py +++ b/dmoj/decorators.py @@ -1,12 +1,14 @@ import time + def timeit(method): def timed(*args, **kw): ts = time.time() result = method(*args, **kw) te = time.time() - if 'log_time' in kw: - name = kw.get('log_name', method.__name__.upper()) - kw['log_time'][name] = int((te - ts) * 1000) + if "log_time" in kw: + name = kw.get("log_name", method.__name__.upper()) + kw["log_time"][name] = int((te - ts) * 1000) return result - return timed \ No newline at end of file + + return timed diff --git a/dmoj/settings.py b/dmoj/settings.py index 634b6e4..d206186 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0' +SECRET_KEY = "5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -30,8 +30,8 @@ DEBUG = True ALLOWED_HOSTS = [] SITE_ID = 1 -SITE_NAME = 'LQDOJ' -SITE_LONG_NAME = 'LQDOJ: Le Quy Don Online Judge' +SITE_NAME = "LQDOJ" +SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge" SITE_ADMIN_EMAIL = False DMOJ_REQUIRE_STAFF_2FA = True @@ -44,13 +44,15 @@ DMOJ_SSL = 0 # Refer to dmoj.ca/post/103-point-system-rework DMOJ_PP_STEP = 0.95 DMOJ_PP_ENTRIES = 100 -DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997 ** n) # noqa: E731 +DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731 -NODEJS = '/usr/bin/node' -EXIFTOOL = '/usr/bin/exiftool' -ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3' -SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' -DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' +NODEJS = "/usr/bin/node" +EXIFTOOL = "/usr/bin/exiftool" +ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3" +SELECT2_JS_URL = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js" +DEFAULT_SELECT2_CSS = ( + "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" +) DMOJ_CAMO_URL = None DMOJ_CAMO_KEY = None @@ -74,14 +76,14 @@ DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_USER_MAX_ORGANIZATION_COUNT = 10 DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 -DMOJ_PDF_PROBLEM_CACHE = '' +DMOJ_PDF_PROBLEM_CACHE = "" DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir() DMOJ_STATS_SUBMISSION_RESULT_COLORS = { - 'TLE': '#a3bcbd', - 'AC': '#00a92a', - 'WA': '#ed4420', - 'CE': '#42586d', - 'ERR': '#ffa71c', + "TLE": "#a3bcbd", + "AC": "#00a92a", + "WA": "#ed4420", + "CE": "#42586d", + "ERR": "#ffa71c", } MARKDOWN_STYLES = {} @@ -90,14 +92,14 @@ MARKDOWN_DEFAULT_STYLE = {} MATHOID_URL = False MATHOID_GZIP = False MATHOID_MML_CACHE = None -MATHOID_CSS_CACHE = 'default' -MATHOID_DEFAULT_TYPE = 'auto' +MATHOID_CSS_CACHE = "default" +MATHOID_DEFAULT_TYPE = "auto" MATHOID_MML_CACHE_TTL = 86400 -MATHOID_CACHE_ROOT = tempfile.gettempdir() + '/mathoidCache' +MATHOID_CACHE_ROOT = tempfile.gettempdir() + "/mathoidCache" MATHOID_CACHE_URL = False TEXOID_GZIP = False -TEXOID_META_CACHE = 'default' +TEXOID_META_CACHE = "default" TEXOID_META_CACHE_TTL = 86400 DMOJ_NEWSLETTER_ID_ON_REGISTER = 1 @@ -110,31 +112,33 @@ TIMEZONE_MAP = None TIMEZONE_DETECT_BACKEND = None TERMS_OF_SERVICE_URL = None -DEFAULT_USER_LANGUAGE = 'PY3' +DEFAULT_USER_LANGUAGE = "PY3" -PHANTOMJS = '' +PHANTOMJS = "" PHANTOMJS_PDF_ZOOM = 0.75 PHANTOMJS_PDF_TIMEOUT = 5.0 -PHANTOMJS_PAPER_SIZE = 'Letter' +PHANTOMJS_PAPER_SIZE = "Letter" -SLIMERJS = '' +SLIMERJS = "" SLIMERJS_PDF_ZOOM = 0.75 -SLIMERJS_FIREFOX_PATH = '' -SLIMERJS_PAPER_SIZE = 'Letter' +SLIMERJS_FIREFOX_PATH = "" +SLIMERJS_PAPER_SIZE = "Letter" -PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer' -PUPPETEER_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' +SELENIUM_CHROMEDRIVER_PATH = "chromedriver" -PYGMENT_THEME = 'pygment-github.css' +PYGMENT_THEME = "pygment-github.css" INLINE_JQUERY = True INLINE_FONTAWESOME = True -JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js' -FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css' -DMOJ_CANONICAL = '' +JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" +FONTAWESOME_CSS = ( + "//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" +) +DMOJ_CANONICAL = "" # Application definition @@ -146,130 +150,130 @@ except ImportError: pass else: del wpadmin - INSTALLED_APPS += ('wpadmin',) + INSTALLED_APPS += ("wpadmin",) WPADMIN = { - 'admin': { - 'title': 'LQDOJ Admin', - 'menu': { - 'top': 'wpadmin.menu.menus.BasicTopMenu', - 'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard', + "admin": { + "title": "LQDOJ Admin", + "menu": { + "top": "wpadmin.menu.menus.BasicTopMenu", + "left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard", }, - 'custom_menu': [ + "custom_menu": [ { - 'model': 'judge.Problem', - 'icon': 'fa-question-circle', - 'children': [ - 'judge.ProblemGroup', - 'judge.ProblemType', - 'judge.ProblemPointsVote', + "model": "judge.Problem", + "icon": "fa-question-circle", + "children": [ + "judge.ProblemGroup", + "judge.ProblemType", + "judge.ProblemPointsVote", ], }, { - 'model': 'judge.Submission', - 'icon': 'fa-check-square-o', - 'children': [ - 'judge.Language', - 'judge.Judge', + "model": "judge.Submission", + "icon": "fa-check-square-o", + "children": [ + "judge.Language", + "judge.Judge", ], }, { - 'model': 'judge.Contest', - 'icon': 'fa-bar-chart', - 'children': [ - 'judge.ContestParticipation', - 'judge.ContestTag', + "model": "judge.Contest", + "icon": "fa-bar-chart", + "children": [ + "judge.ContestParticipation", + "judge.ContestTag", ], }, { - 'model': 'auth.User', - 'icon': 'fa-user', - 'children': [ - 'auth.Group', - 'registration.RegistrationProfile', + "model": "auth.User", + "icon": "fa-user", + "children": [ + "auth.Group", + "registration.RegistrationProfile", ], }, { - 'model': 'judge.Profile', - 'icon': 'fa-user-plus', - 'children': [ - 'judge.Organization', - 'judge.OrganizationRequest', + "model": "judge.Profile", + "icon": "fa-user-plus", + "children": [ + "judge.Organization", + "judge.OrganizationRequest", ], }, { - 'model': 'judge.NavigationBar', - 'icon': 'fa-bars', - 'children': [ - 'judge.MiscConfig', - 'judge.License', - 'sites.Site', - 'redirects.Redirect', + "model": "judge.NavigationBar", + "icon": "fa-bars", + "children": [ + "judge.MiscConfig", + "judge.License", + "sites.Site", + "redirects.Redirect", ], }, - ('judge.BlogPost', 'fa-rss-square'), - ('judge.Comment', 'fa-comment-o'), - ('judge.Ticket', 'fa-exclamation-circle'), - ('flatpages.FlatPage', 'fa-file-text-o'), - ('judge.Solution', 'fa-pencil'), + ("judge.BlogPost", "fa-rss-square"), + ("judge.Comment", "fa-comment-o"), + ("judge.Ticket", "fa-exclamation-circle"), + ("flatpages.FlatPage", "fa-file-text-o"), + ("judge.Solution", "fa-pencil"), ], - 'dashboard': { - 'breadcrumbs': True, + "dashboard": { + "breadcrumbs": True, }, }, } INSTALLED_APPS += ( - 'django.contrib.admin', - 'judge', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.flatpages', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.redirects', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'registration', - 'mptt', - 'reversion', - 'reversion_compare', - 'django_social_share', - 'social_django', - 'compressor', - 'django_ace', - 'pagedown', - 'sortedm2m', - 'statici18n', - 'impersonate', - 'django_jinja', - 'chat_box', - 'newsletter', - 'django.forms', + "django.contrib.admin", + "judge", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.flatpages", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.redirects", + "django.contrib.staticfiles", + "django.contrib.sites", + "django.contrib.sitemaps", + "registration", + "mptt", + "reversion", + "reversion_compare", + "django_social_share", + "social_django", + "compressor", + "django_ace", + "pagedown", + "sortedm2m", + "statici18n", + "impersonate", + "django_jinja", + "chat_box", + "newsletter", + "django.forms", ) MIDDLEWARE = ( - 'judge.middleware.ShortCircuitMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'judge.middleware.DMOJLoginMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'judge.user_log.LogUserAccessMiddleware', - 'judge.timezone.TimezoneMiddleware', - 'impersonate.middleware.ImpersonateMiddleware', - 'judge.middleware.DMOJImpersonationMiddleware', - 'judge.middleware.ContestMiddleware', - 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', - 'judge.social_auth.SocialAuthExceptionMiddleware', - 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', + "judge.middleware.ShortCircuitMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "judge.middleware.DMOJLoginMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "judge.user_log.LogUserAccessMiddleware", + "judge.timezone.TimezoneMiddleware", + "impersonate.middleware.ImpersonateMiddleware", + "judge.middleware.DMOJImpersonationMiddleware", + "judge.middleware.ContestMiddleware", + "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", + "judge.social_auth.SocialAuthExceptionMiddleware", + "django.contrib.redirects.middleware.RedirectFallbackMiddleware", ) -FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' +FORM_RENDERER = "django.forms.renderers.TemplatesSetting" IMPERSONATE_REQUIRE_SUPERUSER = True IMPERSONATE_DISABLE_LOGGING = True @@ -278,226 +282,229 @@ ACCOUNT_ACTIVATION_DAYS = 7 AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'judge.utils.pwned.PwnedPasswordsValidator', + "NAME": "judge.utils.pwned.PwnedPasswordsValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342'] +SILENCED_SYSTEM_CHECKS = ["urls.W002", "fields.W342"] -ROOT_URLCONF = 'dmoj.urls' -LOGIN_REDIRECT_URL = '/user' -WSGI_APPLICATION = 'dmoj.wsgi.application' +ROOT_URLCONF = "dmoj.urls" +LOGIN_REDIRECT_URL = "/user" +WSGI_APPLICATION = "dmoj.wsgi.application" TEMPLATES = [ { - 'BACKEND': 'django_jinja.backend.Jinja2', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django_jinja.backend.Jinja2", + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'APP_DIRS': False, - 'OPTIONS': { - 'match_extension': ('.html', '.txt'), - 'match_regex': '^(?!admin/)', - 'context_processors': [ - 'django.template.context_processors.media', - 'django.template.context_processors.tz', - 'django.template.context_processors.i18n', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', - 'judge.template_context.comet_location', - 'judge.template_context.get_resource', - 'judge.template_context.general_info', - 'judge.template_context.site', - 'judge.template_context.site_name', - 'judge.template_context.misc_config', - 'judge.template_context.math_setting', - 'social_django.context_processors.backends', - 'social_django.context_processors.login_redirect', + "APP_DIRS": False, + "OPTIONS": { + "match_extension": (".html", ".txt"), + "match_regex": "^(?!admin/)", + "context_processors": [ + "django.template.context_processors.media", + "django.template.context_processors.tz", + "django.template.context_processors.i18n", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + "judge.template_context.comet_location", + "judge.template_context.get_resource", + "judge.template_context.general_info", + "judge.template_context.site", + "judge.template_context.site_name", + "judge.template_context.misc_config", + "judge.template_context.math_setting", + "social_django.context_processors.backends", + "social_django.context_processors.login_redirect", ], - 'autoescape': select_autoescape(['html', 'xml']), - 'trim_blocks': True, - 'lstrip_blocks': True, - 'extensions': DEFAULT_EXTENSIONS + [ - 'compressor.contrib.jinja2ext.CompressorExtension', - 'judge.jinja2.DMOJExtension', - 'judge.jinja2.spaceless.SpacelessExtension', + "autoescape": select_autoescape(["html", "xml"]), + "trim_blocks": True, + "lstrip_blocks": True, + "extensions": DEFAULT_EXTENSIONS + + [ + "compressor.contrib.jinja2ext.CompressorExtension", + "judge.jinja2.DMOJExtension", + "judge.jinja2.spaceless.SpacelessExtension", ], }, }, { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.media', - 'django.template.context_processors.tz', - 'django.template.context_processors.i18n', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.media", + "django.template.context_processors.tz", + "django.template.context_processors.i18n", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", ], }, }, ] LOCALE_PATHS = [ - os.path.join(BASE_DIR, 'locale'), + os.path.join(BASE_DIR, "locale"), ] LANGUAGES = [ - ('de', _('German')), - ('en', _('English')), - ('es', _('Spanish')), - ('fr', _('French')), - ('hr', _('Croatian')), - ('hu', _('Hungarian')), - ('ja', _('Japanese')), - ('ko', _('Korean')), - ('pt', _('Brazilian Portuguese')), - ('ro', _('Romanian')), - ('ru', _('Russian')), - ('sr-latn', _('Serbian (Latin)')), - ('tr', _('Turkish')), - ('vi', _('Vietnamese')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), + ("de", _("German")), + ("en", _("English")), + ("es", _("Spanish")), + ("fr", _("French")), + ("hr", _("Croatian")), + ("hu", _("Hungarian")), + ("ja", _("Japanese")), + ("ko", _("Korean")), + ("pt", _("Brazilian Portuguese")), + ("ro", _("Romanian")), + ("ru", _("Russian")), + ("sr-latn", _("Serbian (Latin)")), + ("tr", _("Turkish")), + ("vi", _("Vietnamese")), + ("zh-hans", _("Simplified Chinese")), + ("zh-hant", _("Traditional Chinese")), ] MARKDOWN_ADMIN_EDITABLE_STYLE = { - 'safe_mode': False, - 'use_camo': True, - 'texoid': True, - 'math': True, + "safe_mode": False, + "use_camo": True, + "texoid": True, + "math": True, } MARKDOWN_DEFAULT_STYLE = { - 'safe_mode': True, - 'nofollow': True, - 'use_camo': True, - 'math': True, + "safe_mode": True, + "nofollow": True, + "use_camo": True, + "math": True, } MARKDOWN_USER_LARGE_STYLE = { - 'safe_mode': True, - 'nofollow': True, - 'use_camo': True, - 'math': True, + "safe_mode": True, + "nofollow": True, + "use_camo": True, + "math": True, } MARKDOWN_STYLES = { - 'comment': MARKDOWN_DEFAULT_STYLE, - 'self-description': MARKDOWN_USER_LARGE_STYLE, - 'problem': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'contest': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'language': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'license': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'judge': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'blog': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'solution': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'contest_tag': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'organization-about': MARKDOWN_USER_LARGE_STYLE, - 'ticket': MARKDOWN_USER_LARGE_STYLE, + "comment": MARKDOWN_DEFAULT_STYLE, + "self-description": MARKDOWN_USER_LARGE_STYLE, + "problem": MARKDOWN_ADMIN_EDITABLE_STYLE, + "contest": MARKDOWN_ADMIN_EDITABLE_STYLE, + "language": MARKDOWN_ADMIN_EDITABLE_STYLE, + "license": MARKDOWN_ADMIN_EDITABLE_STYLE, + "judge": MARKDOWN_ADMIN_EDITABLE_STYLE, + "blog": MARKDOWN_ADMIN_EDITABLE_STYLE, + "solution": MARKDOWN_ADMIN_EDITABLE_STYLE, + "contest_tag": MARKDOWN_ADMIN_EDITABLE_STYLE, + "organization-about": MARKDOWN_USER_LARGE_STYLE, + "ticket": MARKDOWN_USER_LARGE_STYLE, } # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, } ENABLE_FTS = False # Bridged configuration -BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)] +BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)] BRIDGED_JUDGE_PROXIES = None -BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)] +BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)] BRIDGED_DJANGO_CONNECT = None # Event Server configuration EVENT_DAEMON_USE = False -EVENT_DAEMON_POST = 'ws://localhost:9997/' -EVENT_DAEMON_GET = 'ws://localhost:9996/' -EVENT_DAEMON_POLL = '/channels/' +EVENT_DAEMON_POST = "ws://localhost:9997/" +EVENT_DAEMON_GET = "ws://localhost:9996/" +EVENT_DAEMON_POLL = "/channels/" EVENT_DAEMON_KEY = None -EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events' -EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww' +EVENT_DAEMON_AMQP_EXCHANGE = "dmoj-events" +EVENT_DAEMON_SUBMISSION_KEY = ( + "6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww" +) # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ # Whatever you do, this better be one of the entries in `LANGUAGES`. -LANGUAGE_CODE = 'vi' -TIME_ZONE = 'Asia/Ho_Chi_Minh' -DEFAULT_USER_TIME_ZONE = 'Asia/Ho_Chi_Minh' +LANGUAGE_CODE = "vi" +TIME_ZONE = "Asia/Ho_Chi_Minh" +DEFAULT_USER_TIME_ZONE = "Asia/Ho_Chi_Minh" USE_I18N = True USE_L10N = True USE_TZ = True # Cookies -SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources') +DMOJ_RESOURCES = os.path.join(BASE_DIR, "resources") STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'resources'), + os.path.join(BASE_DIR, "resources"), ] -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Define a cache CACHES = {} # Authentication AUTHENTICATION_BACKENDS = ( - 'social_core.backends.google.GoogleOAuth2', - 'social_core.backends.facebook.FacebookOAuth2', - 'judge.social_auth.GitHubSecureEmailOAuth2', - 'django.contrib.auth.backends.ModelBackend', + "social_core.backends.google.GoogleOAuth2", + "social_core.backends.facebook.FacebookOAuth2", + "judge.social_auth.GitHubSecureEmailOAuth2", + "django.contrib.auth.backends.ModelBackend", ) SOCIAL_AUTH_PIPELINE = ( - 'social_core.pipeline.social_auth.social_details', - 'social_core.pipeline.social_auth.social_uid', - 'social_core.pipeline.social_auth.auth_allowed', - 'judge.social_auth.verify_email', - 'social_core.pipeline.social_auth.social_user', - 'social_core.pipeline.user.get_username', - 'social_core.pipeline.social_auth.associate_by_email', - 'judge.social_auth.choose_username', - 'social_core.pipeline.user.create_user', - 'judge.social_auth.make_profile', - 'social_core.pipeline.social_auth.associate_user', - 'social_core.pipeline.social_auth.load_extra_data', - 'social_core.pipeline.user.user_details', + "social_core.pipeline.social_auth.social_details", + "social_core.pipeline.social_auth.social_uid", + "social_core.pipeline.social_auth.auth_allowed", + "judge.social_auth.verify_email", + "social_core.pipeline.social_auth.social_user", + "social_core.pipeline.user.get_username", + "social_core.pipeline.social_auth.associate_by_email", + "judge.social_auth.choose_username", + "social_core.pipeline.user.create_user", + "judge.social_auth.make_profile", + "social_core.pipeline.social_auth.associate_user", + "social_core.pipeline.social_auth.load_extra_data", + "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_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 -SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username' +SOCIAL_AUTH_SLUGIFY_FUNCTION = "judge.social_auth.slugify_username" JUDGE_AMQP_PATH = None @@ -529,7 +536,7 @@ NEWSLETTER_BATCH_SIZE = 100 REGISTER_NAME_URL = None try: - with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f: + with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: exec(f.read(), globals()) except IOError: pass diff --git a/dmoj/throttle_mail.py b/dmoj/throttle_mail.py index 7346a21..047e261 100644 --- a/dmoj/throttle_mail.py +++ b/dmoj/throttle_mail.py @@ -8,8 +8,8 @@ DEFAULT_THROTTLE = (10, 60) def new_email(): - cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1]) - return cache.incr('error_email_throttle') + cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1]) + return cache.incr("error_email_throttle") class ThrottledEmailHandler(AdminEmailHandler): diff --git a/dmoj/urls.py b/dmoj/urls.py index b0829d5..fedaace 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -14,429 +14,1076 @@ from django.views.generic import RedirectView from django.contrib.auth.decorators import login_required -from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed +from judge.feed import ( + AtomBlogFeed, + AtomCommentFeed, + AtomProblemFeed, + BlogFeed, + CommentFeed, + ProblemFeed, +) from judge.forms import CustomAuthenticationForm -from judge.sitemap import BlogPostSitemap, ContestSitemap, HomePageSitemap, OrganizationSitemap, ProblemSitemap, \ - SolutionSitemap, UrlSitemap, UserSitemap -from judge.views import TitledTemplateView, about, api, blog, comment, contests, language, license, mailgun, \ - notification, organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, \ - ticket, totp, user, volunteer, widgets, internal -from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ - problem_data_file, problem_init_view, ProblemZipUploadView +from judge.sitemap import ( + BlogPostSitemap, + ContestSitemap, + HomePageSitemap, + OrganizationSitemap, + ProblemSitemap, + SolutionSitemap, + UrlSitemap, + UserSitemap, +) +from judge.views import ( + TitledTemplateView, + about, + api, + blog, + comment, + contests, + language, + license, + mailgun, + notification, + organization, + preview, + problem, + problem_manage, + ranked_submission, + register, + stats, + status, + submission, + tasks, + ticket, + totp, + user, + volunteer, + widgets, + internal, +) +from judge.views.problem_data import ( + ProblemDataView, + ProblemSubmissionDiff, + problem_data_file, + problem_init_view, + ProblemZipUploadView, +) from judge.views.register import ActivationView, RegistrationView -from judge.views.select2 import AssigneeSelect2View, ChatUserSearchSelect2View, CommentSelect2View, \ - ContestSelect2View, ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \ - UserSearchSelect2View, UserSelect2View +from judge.views.select2 import ( + AssigneeSelect2View, + ChatUserSearchSelect2View, + CommentSelect2View, + ContestSelect2View, + ContestUserSearchSelect2View, + OrganizationSelect2View, + ProblemSelect2View, + TicketUserSelect2View, + UserSearchSelect2View, + UserSelect2View, +) admin.autodiscover() register_patterns = [ - url(r'^activate/complete/$', - TitledTemplateView.as_view(template_name='registration/activation_complete.html', - title='Activation Successful!'), - name='registration_activation_complete'), + url( + r"^activate/complete/$", + TitledTemplateView.as_view( + template_name="registration/activation_complete.html", + title="Activation Successful!", + ), + name="registration_activation_complete", + ), # Activation keys get matched by \w+ instead of the more specific # [a-fA-F0-9]{40} because a bad activation key should still get to the view; # that way it can return a sensible "invalid key" message instead of a # confusing 404. - url(r'^activate/(?P\w+)/$', - ActivationView.as_view(title='Activation key invalid'), - name='registration_activate'), - url(r'^register/$', - RegistrationView.as_view(title='Register'), - name='registration_register'), - url(r'^register/complete/$', - TitledTemplateView.as_view(template_name='registration/registration_complete.html', - title='Registration Completed'), - name='registration_complete'), - url(r'^register/closed/$', - TitledTemplateView.as_view(template_name='registration/registration_closed.html', - title='Registration not allowed'), - name='registration_disallowed'), - url(r'^login/$', auth_views.LoginView.as_view( - template_name='registration/login.html', - extra_context={'title': _('Login')}, - authentication_form=CustomAuthenticationForm, - redirect_authenticated_user=True, - ), name='auth_login'), - url(r'^logout/$', user.UserLogoutView.as_view(), name='auth_logout'), - url(r'^password/change/$', auth_views.PasswordChangeView.as_view( - template_name='registration/password_change_form.html', - ), name='password_change'), - url(r'^password/change/done/$', auth_views.PasswordChangeDoneView.as_view( - template_name='registration/password_change_done.html', - ), name='password_change_done'), - url(r'^password/reset/$', auth_views.PasswordResetView.as_view( - template_name='registration/password_reset.html', - html_email_template_name='registration/password_reset_email.html', - email_template_name='registration/password_reset_email.txt', - ), name='password_reset'), - url(r'^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', + url( + r"^activate/(?P\w+)/$", + ActivationView.as_view(title="Activation key invalid"), + name="registration_activate", + ), + url( + r"^register/$", + RegistrationView.as_view(title="Register"), + name="registration_register", + ), + url( + r"^register/complete/$", + TitledTemplateView.as_view( + template_name="registration/registration_complete.html", + title="Registration Completed", + ), + name="registration_complete", + ), + url( + r"^register/closed/$", + TitledTemplateView.as_view( + template_name="registration/registration_closed.html", + title="Registration not allowed", + ), + name="registration_disallowed", + ), + url( + r"^login/$", + auth_views.LoginView.as_view( + template_name="registration/login.html", + extra_context={"title": _("Login")}, + authentication_form=CustomAuthenticationForm, + redirect_authenticated_user=True, + ), + name="auth_login", + ), + url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"), + url( + r"^password/change/$", + auth_views.PasswordChangeView.as_view( + template_name="registration/password_change_form.html", + ), + name="password_change", + ), + url( + r"^password/change/done/$", + auth_views.PasswordChangeDoneView.as_view( + template_name="registration/password_change_done.html", + ), + name="password_change_done", + ), + url( + r"^password/reset/$", + auth_views.PasswordResetView.as_view( + template_name="registration/password_reset.html", + html_email_template_name="registration/password_reset_email.html", + email_template_name="registration/password_reset_email.txt", + ), + name="password_reset", + ), + url( + r"^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$", auth_views.PasswordResetConfirmView.as_view( - template_name='registration/password_reset_confirm.html', - ), name='password_reset_confirm'), - url(r'^password/reset/complete/$', auth_views.PasswordResetCompleteView.as_view( - template_name='registration/password_reset_complete.html', - ), name='password_reset_complete'), - url(r'^password/reset/done/$', auth_views.PasswordResetDoneView.as_view( - template_name='registration/password_reset_done.html', - ), name='password_reset_done'), - url(r'^social/error/$', register.social_auth_error, name='social_auth_error'), - - url(r'^2fa/$', totp.TOTPLoginView.as_view(), name='login_2fa'), - url(r'^2fa/enable/$', totp.TOTPEnableView.as_view(), name='enable_2fa'), - url(r'^2fa/disable/$', totp.TOTPDisableView.as_view(), name='disable_2fa'), + template_name="registration/password_reset_confirm.html", + ), + name="password_reset_confirm", + ), + url( + r"^password/reset/complete/$", + auth_views.PasswordResetCompleteView.as_view( + template_name="registration/password_reset_complete.html", + ), + name="password_reset_complete", + ), + url( + r"^password/reset/done/$", + auth_views.PasswordResetDoneView.as_view( + template_name="registration/password_reset_done.html", + ), + name="password_reset_done", + ), + url(r"^social/error/$", register.social_auth_error, name="social_auth_error"), + url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"), + url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"), + url(r"^2fa/disable/$", totp.TOTPDisableView.as_view(), name="disable_2fa"), ] def exception(request): if not request.user.is_superuser: raise Http404() - raise RuntimeError('@Xyene asked me to cause this') + raise RuntimeError("@Xyene asked me to cause this") def paged_list_view(view, name, **kwargs): - return include([ - url(r'^$', view.as_view(**kwargs), name=name), - url(r'^(?P\d+)$', view.as_view(**kwargs), name=name), - ]) + return include( + [ + url(r"^$", view.as_view(**kwargs), name=name), + url(r"^(?P\d+)$", view.as_view(**kwargs), name=name), + ] + ) urlpatterns = [ - url(r'^$', blog.PostList.as_view(template_name='home.html', title=_('Home')), kwargs={'page': 1}, name='home'), - url(r'^500/$', exception), - url(r'^admin/', admin.site.urls), - url(r'^i18n/', include('django.conf.urls.i18n')), - url(r'^accounts/', include(register_patterns)), - url(r'^', include('social_django.urls')), - - url(r'^feed/', include([ - url(r'^tickets/$', blog.TicketFeed.as_view(), name='ticket_feed'), - url(r'^comments/$', blog.CommentFeed.as_view(), name='comment_feed'), - ])), - url(r'^problems/', paged_list_view(problem.ProblemList, 'problem_list')), - url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'), - url(r'^problems/feed/', paged_list_view(problem.ProblemFeed, 'problem_feed', feed_type='for_you')), - url(r'^problems/feed/new/', paged_list_view(problem.ProblemFeed, 'problem_feed_new', feed_type='new')), - url(r'^problems/feed/volunteer/', paged_list_view(problem.ProblemFeed, 'problem_feed_volunteer', feed_type='volunteer')), - - url(r'^problem/(?P[^/]+)', include([ - url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), - url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'), - url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'), - url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'), - url(r'^/pdf/(?P[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'), - url(r'^/clone', problem.ProblemClone.as_view(), name='problem_clone'), - url(r'^/submit$', problem.problem_submit, name='problem_submit'), - url(r'^/resubmit/(?P\d+)$', problem.problem_submit, name='problem_submit'), - - url(r'^/rank/', paged_list_view(ranked_submission.RankedSubmissions, 'ranked_submissions')), - url(r'^/submissions/', paged_list_view(submission.ProblemSubmissions, 'chronological_submissions')), - url(r'^/submissions/(?P\w+)/', paged_list_view(submission.UserProblemSubmissions, 'user_submissions')), - - url(r'^/$', lambda _, problem: HttpResponsePermanentRedirect(reverse('problem_detail', args=[problem]))), - - 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.+)$', problem_data_file, name='problem_data_file'), - - url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), - url(r'^/tickets/new$', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'), - - url(r'^/vote$', problem.Vote.as_view(), name='vote'), - - url(r'^/manage/submission', include([ - url('^$', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'), - url('^/action$', problem_manage.ActionSubmissionsView.as_view(), name='problem_submissions_action'), - url('^/action/preview$', problem_manage.PreviewActionSubmissionsView.as_view(), - name='problem_submissions_rejudge_preview'), - url('^/rejudge/success/(?P[A-Za-z0-9-]*)$', problem_manage.rejudge_success, - name='problem_submissions_rejudge_success'), - url('^/rescore/all$', problem_manage.RescoreAllSubmissionsView.as_view(), - name='problem_submissions_rescore_all'), - url('^/rescore/success/(?P[A-Za-z0-9-]*)$', problem_manage.rescore_success, - name='problem_submissions_rescore_success'), - ])), - ])), - - url(r'^submissions/', paged_list_view(submission.AllSubmissions, 'all_submissions')), - url(r'^submissions/user/(?P\w+)/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions')), - - url(r'^src/(?P\d+)$', submission.SubmissionSource.as_view(), name='submission_source'), - url(r'^src/(?P\d+)/raw$', submission.SubmissionSourceRaw.as_view(), name='submission_source_raw'), - - url(r'^submission/(?P\d+)', include([ - url(r'^$', submission.SubmissionStatus.as_view(), name='submission_status'), - url(r'^/abort$', submission.abort_submission, name='submission_abort'), - url(r'^/html$', submission.single_submission), - ])), - - url(r'^users/', include([ - url(r'^$', user.users, name='user_list'), - url(r'^(?P\d+)$', lambda request, page: - HttpResponsePermanentRedirect('%s?page=%s' % (reverse('user_list'), page))), - url(r'^find$', user.user_ranking_redirect, name='user_ranking_redirect'), - ])), - - url(r'^user$', user.UserAboutPage.as_view(), name='user_page'), - url(r'^edit/profile/$', user.edit_profile, name='user_edit_profile'), - url(r'^user/(?P\w+)', include([ - url(r'^$', user.UserAboutPage.as_view(), name='user_page'), - url(r'^/solved', include([ - url(r'^$', user.UserProblemsPage.as_view(), name='user_problems'), - url(r'/ajax$', user.UserPerformancePointsAjax.as_view(), name='user_pp_ajax'), - ])), - url(r'^/submissions/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions_old')), - url(r'^/submissions/', lambda _, user: - HttpResponsePermanentRedirect(reverse('all_user_submissions', args=[user]))), - - url(r'^/$', lambda _, user: HttpResponsePermanentRedirect(reverse('user_page', args=[user]))), - ])), - - url(r'^comments/upvote/$', comment.upvote_comment, name='comment_upvote'), - url(r'^comments/downvote/$', comment.downvote_comment, name='comment_downvote'), - url(r'^comments/hide/$', comment.comment_hide, name='comment_hide'), - url(r'^comments/(?P\d+)/', include([ - url(r'^edit$', comment.CommentEdit.as_view(), name='comment_edit'), - url(r'^history/ajax$', comment.CommentRevisionAjax.as_view(), name='comment_revision_ajax'), - url(r'^edit/ajax$', comment.CommentEditAjax.as_view(), name='comment_edit_ajax'), - url(r'^votes/ajax$', comment.CommentVotesAjax.as_view(), name='comment_votes_ajax'), - url(r'^render$', comment.CommentContent.as_view(), name='comment_content'), - ])), - - url(r'^contests/', paged_list_view(contests.ContestList, 'contest_list')), - url(r'^contests/(?P\d+)/(?P\d+)/$', contests.ContestCalendar.as_view(), name='contest_calendar'), - url(r'^contests/tag/(?P[a-z-]+)', include([ - url(r'^$', contests.ContestTagDetail.as_view(), name='contest_tag'), - url(r'^/ajax$', contests.ContestTagDetailAjax.as_view(), name='contest_tag_ajax'), - ])), - - url(r'^contest/(?P\w+)', include([ - url(r'^$', contests.ContestDetail.as_view(), name='contest_view'), - url(r'^/moss$', contests.ContestMossView.as_view(), name='contest_moss'), - url(r'^/moss/delete$', contests.ContestMossDelete.as_view(), name='contest_moss_delete'), - url(r'^/clone$', contests.ContestClone.as_view(), name='contest_clone'), - url(r'^/ranking/$', contests.ContestRanking.as_view(), name='contest_ranking'), - url(r'^/ranking/ajax$', contests.contest_ranking_ajax, name='contest_ranking_ajax'), - url(r'^/join$', contests.ContestJoin.as_view(), name='contest_join'), - url(r'^/leave$', contests.ContestLeave.as_view(), name='contest_leave'), - url(r'^/stats$', contests.ContestStats.as_view(), name='contest_stats'), - - url(r'^/rank/(?P\w+)/', - paged_list_view(ranked_submission.ContestRankedSubmission, 'contest_ranked_submissions')), - - url(r'^/submissions/(?P\w+)/(?P\w+)/', - paged_list_view(submission.UserContestSubmissions, 'contest_user_submissions')), - - url(r'^/participations$', contests.ContestParticipationList.as_view(), name='contest_participation_own'), - url(r'^/participations/(?P\w+)$', - contests.ContestParticipationList.as_view(), name='contest_participation'), - 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]))), - ])), - - url(r'^organizations/$', organization.OrganizationList.as_view(), name='organization_list'), - url(r'^organization/(?P\d+)-(?P[\w-]*)', include([ - url(r'^$', organization.OrganizationHome.as_view(), name='organization_home'), - url(r'^/users$', organization.OrganizationUsers.as_view(), name='organization_users'), - url(r'^/join$', organization.JoinOrganization.as_view(), name='join_organization'), - url(r'^/leave$', organization.LeaveOrganization.as_view(), name='leave_organization'), - url(r'^/edit$', organization.EditOrganization.as_view(), name='edit_organization'), - url(r'^/kick$', organization.KickUserWidgetView.as_view(), name='organization_user_kick'), - - url(r'^/request$', organization.RequestJoinOrganization.as_view(), name='request_organization'), - url(r'^/request/(?P\d+)$', organization.OrganizationRequestDetail.as_view(), - name='request_organization_detail'), - url(r'^/requests/', include([ - url(r'^pending$', organization.OrganizationRequestView.as_view(), name='organization_requests_pending'), - url(r'^log$', organization.OrganizationRequestLog.as_view(), name='organization_requests_log'), - url(r'^approved$', organization.OrganizationRequestLog.as_view(states=('A',), tab='approved'), - name='organization_requests_approved'), - url(r'^rejected$', organization.OrganizationRequestLog.as_view(states=('R',), tab='rejected'), - name='organization_requests_rejected'), - ])), - - url(r'^/$', lambda _, pk, slug: HttpResponsePermanentRedirect(reverse('organization_home', args=[pk, slug]))), - ])), - - url(r'^runtimes/$', language.LanguageList.as_view(), name='runtime_list'), - url(r'^runtimes/matrix/$', status.version_matrix, name='version_matrix'), - url(r'^status/$', status.status_all, name='status_all'), - - url(r'^api/', include([ - url(r'^contest/list$', api.api_v1_contest_list), - url(r'^contest/info/(\w+)$', api.api_v1_contest_detail), - url(r'^problem/list$', api.api_v1_problem_list), - url(r'^problem/info/(\w+)$', api.api_v1_problem_info), - url(r'^user/list$', api.api_v1_user_list), - url(r'^user/info/(\w+)$', api.api_v1_user_info), - url(r'^user/submissions/(\w+)$', api.api_v1_user_submissions), - ])), - - url(r'^blog/', paged_list_view(blog.PostList, 'blog_post_list')), - url(r'^post/(?P\d+)-(?P.*)$', blog.PostView.as_view(), name='blog_post'), - - url(r'^license/(?P[-\w.]+)$', license.LicenseDetail.as_view(), name='license'), - - url(r'^mailgun/mail_activate/$', mailgun.MailgunActivationView.as_view(), name='mailgun_activate'), - - url(r'^widgets/', include([ - url(r'^contest_mode$', contests.update_contest_mode, name='contest_mode_ajax'), - url(r'^rejudge$', widgets.rejudge_submission, name='submission_rejudge'), - url(r'^single_submission$', submission.single_submission_query, name='submission_single_query'), - url(r'^submission_testcases$', submission.SubmissionTestCaseQuery.as_view(), name='submission_testcases_query'), - url(r'^detect_timezone$', widgets.DetectTimezone.as_view(), name='detect_timezone'), - url(r'^status-table$', status.status_table, name='status_table'), - - url(r'^template$', problem.LanguageTemplateAjax.as_view(), name='language_template_ajax'), - - 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\w+)$', ContestUserSearchSelect2View.as_view(), - name='contest_user_search_select2_ajax'), - url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'), - url(r'^ticket_assignee$', AssigneeSelect2View.as_view(), name='ticket_assignee_select2_ajax'), - ])), - - url(r'^preview/', include([ - url(r'^problem$', preview.ProblemMarkdownPreviewView.as_view(), name='problem_preview'), - url(r'^blog$', preview.BlogMarkdownPreviewView.as_view(), name='blog_preview'), - url(r'^contest$', preview.ContestMarkdownPreviewView.as_view(), name='contest_preview'), - url(r'^comment$', preview.CommentMarkdownPreviewView.as_view(), name='comment_preview'), - url(r'^profile$', preview.ProfileMarkdownPreviewView.as_view(), name='profile_preview'), - url(r'^organization$', preview.OrganizationMarkdownPreviewView.as_view(), name='organization_preview'), - url(r'^solution$', preview.SolutionMarkdownPreviewView.as_view(), name='solution_preview'), - url(r'^license$', preview.LicenseMarkdownPreviewView.as_view(), name='license_preview'), - url(r'^ticket$', preview.TicketMarkdownPreviewView.as_view(), name='ticket_preview'), - ])), - ])), - - url(r'^feed/', include([ - url(r'^problems/rss/$', ProblemFeed(), name='problem_rss'), - url(r'^problems/atom/$', AtomProblemFeed(), name='problem_atom'), - url(r'^comment/rss/$', CommentFeed(), name='comment_rss'), - url(r'^comment/atom/$', AtomCommentFeed(), name='comment_atom'), - url(r'^blog/rss/$', BlogFeed(), name='blog_rss'), - url(r'^blog/atom/$', AtomBlogFeed(), name='blog_atom'), - ])), - - url(r'^stats/', include([ - url('^language/', include([ - url('^$', stats.language, name='language_stats'), - url('^data/all/$', stats.language_data, name='language_stats_data_all'), - url('^data/ac/$', stats.ac_language_data, name='language_stats_data_ac'), - url('^data/status/$', stats.status_data, name='stats_data_status'), - url('^data/ac_rate/$', stats.ac_rate, name='language_stats_data_ac_rate'), - ])), - ])), - - url(r'^tickets/', include([ - url(r'^$', ticket.TicketList.as_view(), name='ticket_list'), - url(r'^ajax$', ticket.TicketListDataAjax.as_view(), name='ticket_ajax'), - ])), - - url(r'^ticket/(?P\d+)', include([ - url(r'^$', ticket.TicketView.as_view(), name='ticket'), - url(r'^/ajax$', ticket.TicketMessageDataAjax.as_view(), name='ticket_message_ajax'), - url(r'^/open$', ticket.TicketStatusChangeView.as_view(open=True), name='ticket_open'), - url(r'^/close$', ticket.TicketStatusChangeView.as_view(open=False), name='ticket_close'), - url(r'^/notes$', ticket.TicketNotesEditView.as_view(), name='ticket_notes'), - ])), - - url(r'^sitemap\.xml$', sitemap, {'sitemaps': { - 'problem': ProblemSitemap, - 'user': UserSitemap, - 'home': HomePageSitemap, - 'contest': ContestSitemap, - 'organization': OrganizationSitemap, - 'blog': BlogPostSitemap, - 'solutions': SolutionSitemap, - 'pages': UrlSitemap([ - {'location': '/about/', 'priority': 0.9}, - ]), - }}), - - url(r'^judge-select2/', include([ - url(r'^profile/$', UserSelect2View.as_view(), name='profile_select2'), - url(r'^organization/$', OrganizationSelect2View.as_view(), name='organization_select2'), - url(r'^problem/$', ProblemSelect2View.as_view(), name='problem_select2'), - url(r'^contest/$', ContestSelect2View.as_view(), name='contest_select2'), - url(r'^comment/$', CommentSelect2View.as_view(), name='comment_select2'), - ])), - - url(r'^tasks/', include([ - url(r'^status/(?P[A-Za-z0-9-]*)$', tasks.task_status, name='task_status'), - url(r'^ajax_status$', tasks.task_status_ajax, name='task_status_ajax'), - url(r'^success$', tasks.demo_success), - url(r'^failure$', tasks.demo_failure), - url(r'^progress$', tasks.demo_progress), - ])), - - url(r'^about/', about.about, name='about'), - - url(r'^custom_checker_sample/', about.custom_checker_sample, name='custom_checker_sample'), - - url(r'^chat/', include([ - url(r'^(?P\d*)$', login_required(chat.ChatView.as_view()), name='chat'), - url(r'^delete/$', chat.delete_message, name='delete_chat_message'), - url(r'^post/$', chat.post_message, name='post_chat_message'), - url(r'^ajax$', chat.chat_message_ajax, name='chat_message_ajax'), - url(r'^online_status/ajax$', chat.online_status_ajax, name='online_status_ajax'), - url(r'^get_or_create_room$', chat.get_or_create_room, name='get_or_create_room'), - url(r'^update_last_seen$', chat.update_last_seen, name='update_last_seen'), - url(r'^online_status/user/ajax$', chat.user_online_status_ajax, name='user_online_status_ajax'), - url(r'^toggle_ignore/(?P\d+)$', chat.toggle_ignore, name='toggle_ignore'), - url(r'^get_unread_boxes$', chat.get_unread_boxes, name='get_unread_boxes'), - ])), - - url(r'^internal/', include([ - url(r'^problem$', internal.InternalProblem.as_view(), name='internal_problem'), - ])), - - url(r'^notifications/', + url( + r"^$", + blog.PostList.as_view(template_name="home.html", title=_("Home")), + kwargs={"page": 1}, + name="home", + ), + url(r"^500/$", exception), + url(r"^admin/", admin.site.urls), + url(r"^i18n/", include("django.conf.urls.i18n")), + url(r"^accounts/", include(register_patterns)), + url(r"^", include("social_django.urls")), + url( + r"^feed/", + include( + [ + url(r"^tickets/$", blog.TicketFeed.as_view(), name="ticket_feed"), + url(r"^comments/$", blog.CommentFeed.as_view(), name="comment_feed"), + ] + ), + ), + url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")), + url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"), + url( + r"^problems/feed/", + paged_list_view(problem.ProblemFeed, "problem_feed", feed_type="for_you"), + ), + url( + r"^problems/feed/new/", + paged_list_view(problem.ProblemFeed, "problem_feed_new", feed_type="new"), + ), + url( + r"^problems/feed/volunteer/", + paged_list_view( + problem.ProblemFeed, "problem_feed_volunteer", feed_type="volunteer" + ), + ), + url( + r"^problem/(?P[^/]+)", + include( + [ + url(r"^$", problem.ProblemDetail.as_view(), name="problem_detail"), + url( + r"^/editorial$", + problem.ProblemSolution.as_view(), + name="problem_editorial", + ), + url(r"^/raw$", problem.ProblemRaw.as_view(), name="problem_raw"), + url(r"^/pdf$", problem.ProblemPdfView.as_view(), name="problem_pdf"), + url( + r"^/pdf/(?P[a-z-]+)$", + problem.ProblemPdfView.as_view(), + name="problem_pdf", + ), + url(r"^/clone", problem.ProblemClone.as_view(), name="problem_clone"), + url(r"^/submit$", problem.problem_submit, name="problem_submit"), + url( + r"^/resubmit/(?P\d+)$", + problem.problem_submit, + name="problem_submit", + ), + url( + r"^/rank/", + paged_list_view( + ranked_submission.RankedSubmissions, "ranked_submissions" + ), + ), + url( + r"^/submissions/", + paged_list_view( + submission.ProblemSubmissions, "chronological_submissions" + ), + ), + url( + r"^/submissions/(?P\w+)/", + paged_list_view( + submission.UserProblemSubmissions, "user_submissions" + ), + ), + url( + r"^/$", + lambda _, problem: HttpResponsePermanentRedirect( + reverse("problem_detail", args=[problem]) + ), + ), + 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.+)$", problem_data_file, name="problem_data_file" + ), + url( + r"^/tickets$", + ticket.ProblemTicketListView.as_view(), + name="problem_ticket_list", + ), + url( + r"^/tickets/new$", + ticket.NewProblemTicketView.as_view(), + name="new_problem_ticket", + ), + url(r"^/vote$", problem.Vote.as_view(), name="vote"), + url( + r"^/manage/submission", + include( + [ + url( + "^$", + problem_manage.ManageProblemSubmissionView.as_view(), + name="problem_manage_submissions", + ), + url( + "^/action$", + problem_manage.ActionSubmissionsView.as_view(), + name="problem_submissions_action", + ), + url( + "^/action/preview$", + problem_manage.PreviewActionSubmissionsView.as_view(), + name="problem_submissions_rejudge_preview", + ), + url( + "^/rejudge/success/(?P[A-Za-z0-9-]*)$", + problem_manage.rejudge_success, + name="problem_submissions_rejudge_success", + ), + url( + "^/rescore/all$", + problem_manage.RescoreAllSubmissionsView.as_view(), + name="problem_submissions_rescore_all", + ), + url( + "^/rescore/success/(?P[A-Za-z0-9-]*)$", + problem_manage.rescore_success, + name="problem_submissions_rescore_success", + ), + ] + ), + ), + ] + ), + ), + url( + r"^submissions/", paged_list_view(submission.AllSubmissions, "all_submissions") + ), + url( + r"^submissions/user/(?P\w+)/", + paged_list_view(submission.AllUserSubmissions, "all_user_submissions"), + ), + url( + r"^src/(?P\d+)$", + submission.SubmissionSource.as_view(), + name="submission_source", + ), + url( + r"^src/(?P\d+)/raw$", + submission.SubmissionSourceRaw.as_view(), + name="submission_source_raw", + ), + url( + r"^submission/(?P\d+)", + include( + [ + url( + r"^$", + submission.SubmissionStatus.as_view(), + name="submission_status", + ), + url(r"^/abort$", submission.abort_submission, name="submission_abort"), + url(r"^/html$", submission.single_submission), + ] + ), + ), + url( + r"^users/", + include( + [ + url(r"^$", user.users, name="user_list"), + url( + r"^(?P\d+)$", + lambda request, page: HttpResponsePermanentRedirect( + "%s?page=%s" % (reverse("user_list"), page) + ), + ), + url( + r"^find$", user.user_ranking_redirect, name="user_ranking_redirect" + ), + ] + ), + ), + url(r"^user$", user.UserAboutPage.as_view(), name="user_page"), + url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"), + url( + r"^user/(?P\w+)", + include( + [ + url(r"^$", user.UserAboutPage.as_view(), name="user_page"), + url( + r"^/solved", + include( + [ + url( + r"^$", + user.UserProblemsPage.as_view(), + name="user_problems", + ), + url( + r"/ajax$", + user.UserPerformancePointsAjax.as_view(), + name="user_pp_ajax", + ), + ] + ), + ), + url( + r"^/submissions/", + paged_list_view( + submission.AllUserSubmissions, "all_user_submissions_old" + ), + ), + url( + r"^/submissions/", + lambda _, user: HttpResponsePermanentRedirect( + reverse("all_user_submissions", args=[user]) + ), + ), + url( + r"^/$", + lambda _, user: HttpResponsePermanentRedirect( + reverse("user_page", args=[user]) + ), + ), + ] + ), + ), + url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"), + url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"), + url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), + url( + r"^comments/(?P\d+)/", + include( + [ + url(r"^edit$", comment.CommentEdit.as_view(), name="comment_edit"), + url( + r"^history/ajax$", + comment.CommentRevisionAjax.as_view(), + name="comment_revision_ajax", + ), + url( + r"^edit/ajax$", + comment.CommentEditAjax.as_view(), + name="comment_edit_ajax", + ), + url( + r"^votes/ajax$", + comment.CommentVotesAjax.as_view(), + name="comment_votes_ajax", + ), + url( + r"^render$", + comment.CommentContent.as_view(), + name="comment_content", + ), + ] + ), + ), + url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), + url( + r"^contests/(?P\d+)/(?P\d+)/$", + contests.ContestCalendar.as_view(), + name="contest_calendar", + ), + url( + r"^contests/tag/(?P[a-z-]+)", + include( + [ + url(r"^$", contests.ContestTagDetail.as_view(), name="contest_tag"), + url( + r"^/ajax$", + contests.ContestTagDetailAjax.as_view(), + name="contest_tag_ajax", + ), + ] + ), + ), + url( + r"^contest/(?P\w+)", + include( + [ + url(r"^$", contests.ContestDetail.as_view(), name="contest_view"), + url( + r"^/moss$", contests.ContestMossView.as_view(), name="contest_moss" + ), + url( + r"^/moss/delete$", + contests.ContestMossDelete.as_view(), + name="contest_moss_delete", + ), + url(r"^/clone$", contests.ContestClone.as_view(), name="contest_clone"), + url( + r"^/ranking/$", + contests.ContestRanking.as_view(), + name="contest_ranking", + ), + url( + r"^/ranking/ajax$", + contests.contest_ranking_ajax, + name="contest_ranking_ajax", + ), + url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"), + url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"), + url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"), + url( + r"^/rank/(?P\w+)/", + paged_list_view( + ranked_submission.ContestRankedSubmission, + "contest_ranked_submissions", + ), + ), + url( + r"^/submissions/(?P\w+)/(?P\w+)/", + paged_list_view( + submission.UserContestSubmissions, "contest_user_submissions" + ), + ), + url( + r"^/participations$", + contests.ContestParticipationList.as_view(), + name="contest_participation_own", + ), + url( + r"^/participations/(?P\w+)$", + contests.ContestParticipationList.as_view(), + name="contest_participation", + ), + 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]) + ), + ), + ] + ), + ), + url( + r"^organizations/$", + organization.OrganizationList.as_view(), + name="organization_list", + ), + url( + r"^organization/(?P\d+)-(?P[\w-]*)", + include( + [ + url( + r"^$", + organization.OrganizationHome.as_view(), + name="organization_home", + ), + url( + r"^/users$", + organization.OrganizationUsers.as_view(), + name="organization_users", + ), + url( + r"^/join$", + organization.JoinOrganization.as_view(), + name="join_organization", + ), + url( + r"^/leave$", + organization.LeaveOrganization.as_view(), + name="leave_organization", + ), + url( + r"^/edit$", + organization.EditOrganization.as_view(), + name="edit_organization", + ), + url( + r"^/kick$", + organization.KickUserWidgetView.as_view(), + name="organization_user_kick", + ), + url( + r"^/request$", + organization.RequestJoinOrganization.as_view(), + name="request_organization", + ), + url( + r"^/request/(?P\d+)$", + organization.OrganizationRequestDetail.as_view(), + name="request_organization_detail", + ), + url( + r"^/requests/", + include( + [ + url( + r"^pending$", + organization.OrganizationRequestView.as_view(), + name="organization_requests_pending", + ), + url( + r"^log$", + organization.OrganizationRequestLog.as_view(), + name="organization_requests_log", + ), + url( + r"^approved$", + organization.OrganizationRequestLog.as_view( + states=("A",), tab="approved" + ), + name="organization_requests_approved", + ), + url( + r"^rejected$", + organization.OrganizationRequestLog.as_view( + states=("R",), tab="rejected" + ), + name="organization_requests_rejected", + ), + ] + ), + ), + url( + r"^/$", + lambda _, pk, slug: HttpResponsePermanentRedirect( + reverse("organization_home", args=[pk, slug]) + ), + ), + ] + ), + ), + url(r"^runtimes/$", language.LanguageList.as_view(), name="runtime_list"), + url(r"^runtimes/matrix/$", status.version_matrix, name="version_matrix"), + url(r"^status/$", status.status_all, name="status_all"), + url( + r"^api/", + include( + [ + url(r"^contest/list$", api.api_v1_contest_list), + url(r"^contest/info/(\w+)$", api.api_v1_contest_detail), + url(r"^problem/list$", api.api_v1_problem_list), + url(r"^problem/info/(\w+)$", api.api_v1_problem_info), + url(r"^user/list$", api.api_v1_user_list), + url(r"^user/info/(\w+)$", api.api_v1_user_info), + url(r"^user/submissions/(\w+)$", api.api_v1_user_submissions), + ] + ), + ), + url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")), + url(r"^post/(?P\d+)-(?P.*)$", blog.PostView.as_view(), name="blog_post"), + url(r"^license/(?P[-\w.]+)$", license.LicenseDetail.as_view(), name="license"), + url( + r"^mailgun/mail_activate/$", + mailgun.MailgunActivationView.as_view(), + name="mailgun_activate", + ), + url( + r"^widgets/", + include( + [ + url( + r"^contest_mode$", + contests.update_contest_mode, + name="contest_mode_ajax", + ), + url( + r"^rejudge$", widgets.rejudge_submission, name="submission_rejudge" + ), + url( + r"^single_submission$", + submission.single_submission_query, + name="submission_single_query", + ), + url( + r"^submission_testcases$", + submission.SubmissionTestCaseQuery.as_view(), + name="submission_testcases_query", + ), + url( + r"^detect_timezone$", + widgets.DetectTimezone.as_view(), + name="detect_timezone", + ), + url(r"^status-table$", status.status_table, name="status_table"), + url( + r"^template$", + problem.LanguageTemplateAjax.as_view(), + name="language_template_ajax", + ), + 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\w+)$", + ContestUserSearchSelect2View.as_view(), + name="contest_user_search_select2_ajax", + ), + url( + r"^ticket_user$", + TicketUserSelect2View.as_view(), + name="ticket_user_select2_ajax", + ), + url( + r"^ticket_assignee$", + AssigneeSelect2View.as_view(), + name="ticket_assignee_select2_ajax", + ), + ] + ), + ), + url( + r"^preview/", + include( + [ + url( + r"^problem$", + preview.ProblemMarkdownPreviewView.as_view(), + name="problem_preview", + ), + url( + r"^blog$", + preview.BlogMarkdownPreviewView.as_view(), + name="blog_preview", + ), + url( + r"^contest$", + preview.ContestMarkdownPreviewView.as_view(), + name="contest_preview", + ), + url( + r"^comment$", + preview.CommentMarkdownPreviewView.as_view(), + name="comment_preview", + ), + url( + r"^profile$", + preview.ProfileMarkdownPreviewView.as_view(), + name="profile_preview", + ), + url( + r"^organization$", + preview.OrganizationMarkdownPreviewView.as_view(), + name="organization_preview", + ), + url( + r"^solution$", + preview.SolutionMarkdownPreviewView.as_view(), + name="solution_preview", + ), + url( + r"^license$", + preview.LicenseMarkdownPreviewView.as_view(), + name="license_preview", + ), + url( + r"^ticket$", + preview.TicketMarkdownPreviewView.as_view(), + name="ticket_preview", + ), + ] + ), + ), + ] + ), + ), + url( + r"^feed/", + include( + [ + url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"), + url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"), + url(r"^comment/rss/$", CommentFeed(), name="comment_rss"), + url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"), + url(r"^blog/rss/$", BlogFeed(), name="blog_rss"), + url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"), + ] + ), + ), + url( + r"^stats/", + include( + [ + url( + "^language/", + include( + [ + url("^$", stats.language, name="language_stats"), + url( + "^data/all/$", + stats.language_data, + name="language_stats_data_all", + ), + url( + "^data/ac/$", + stats.ac_language_data, + name="language_stats_data_ac", + ), + url( + "^data/status/$", + stats.status_data, + name="stats_data_status", + ), + url( + "^data/ac_rate/$", + stats.ac_rate, + name="language_stats_data_ac_rate", + ), + ] + ), + ), + ] + ), + ), + url( + r"^tickets/", + include( + [ + url(r"^$", ticket.TicketList.as_view(), name="ticket_list"), + url(r"^ajax$", ticket.TicketListDataAjax.as_view(), name="ticket_ajax"), + ] + ), + ), + url( + r"^ticket/(?P\d+)", + include( + [ + url(r"^$", ticket.TicketView.as_view(), name="ticket"), + url( + r"^/ajax$", + ticket.TicketMessageDataAjax.as_view(), + name="ticket_message_ajax", + ), + url( + r"^/open$", + ticket.TicketStatusChangeView.as_view(open=True), + name="ticket_open", + ), + url( + r"^/close$", + ticket.TicketStatusChangeView.as_view(open=False), + name="ticket_close", + ), + url( + r"^/notes$", + ticket.TicketNotesEditView.as_view(), + name="ticket_notes", + ), + ] + ), + ), + url( + r"^sitemap\.xml$", + sitemap, + { + "sitemaps": { + "problem": ProblemSitemap, + "user": UserSitemap, + "home": HomePageSitemap, + "contest": ContestSitemap, + "organization": OrganizationSitemap, + "blog": BlogPostSitemap, + "solutions": SolutionSitemap, + "pages": UrlSitemap( + [ + {"location": "/about/", "priority": 0.9}, + ] + ), + } + }, + ), + url( + r"^judge-select2/", + include( + [ + url(r"^profile/$", UserSelect2View.as_view(), name="profile_select2"), + url( + r"^organization/$", + OrganizationSelect2View.as_view(), + name="organization_select2", + ), + url( + r"^problem/$", ProblemSelect2View.as_view(), name="problem_select2" + ), + url( + r"^contest/$", ContestSelect2View.as_view(), name="contest_select2" + ), + url( + r"^comment/$", CommentSelect2View.as_view(), name="comment_select2" + ), + ] + ), + ), + url( + r"^tasks/", + include( + [ + url( + r"^status/(?P[A-Za-z0-9-]*)$", + tasks.task_status, + name="task_status", + ), + url(r"^ajax_status$", tasks.task_status_ajax, name="task_status_ajax"), + url(r"^success$", tasks.demo_success), + url(r"^failure$", tasks.demo_failure), + url(r"^progress$", tasks.demo_progress), + ] + ), + ), + url(r"^about/", about.about, name="about"), + url( + r"^custom_checker_sample/", + about.custom_checker_sample, + name="custom_checker_sample", + ), + url( + r"^chat/", + include( + [ + url( + r"^(?P\d*)$", + login_required(chat.ChatView.as_view()), + name="chat", + ), + url(r"^delete/$", chat.delete_message, name="delete_chat_message"), + url(r"^post/$", chat.post_message, name="post_chat_message"), + url(r"^ajax$", chat.chat_message_ajax, name="chat_message_ajax"), + url( + r"^online_status/ajax$", + chat.online_status_ajax, + name="online_status_ajax", + ), + url( + r"^get_or_create_room$", + chat.get_or_create_room, + name="get_or_create_room", + ), + url( + r"^update_last_seen$", + chat.update_last_seen, + name="update_last_seen", + ), + url( + r"^online_status/user/ajax$", + chat.user_online_status_ajax, + name="user_online_status_ajax", + ), + url( + r"^toggle_ignore/(?P\d+)$", + chat.toggle_ignore, + name="toggle_ignore", + ), + url( + r"^get_unread_boxes$", + chat.get_unread_boxes, + name="get_unread_boxes", + ), + ] + ), + ), + url( + r"^internal/", + include( + [ + url( + r"^problem$", + internal.InternalProblem.as_view(), + name="internal_problem", + ), + ] + ), + ), + url( + r"^notifications/", login_required(notification.NotificationList.as_view()), - 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') - ])), - - url(r'^volunteer/', include([ - url(r'^problem/vote$', volunteer.vote_problem, name='volunteer_problem_vote'), - ])), + 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"), + ] + ), + ), + url( + r"^volunteer/", + include( + [ + url( + r"^problem/vote$", + volunteer.vote_problem, + name="volunteer_problem_vote", + ), + ] + ), + ), ] -favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png', - 'apple-touch-icon-57x57.png', 'apple-touch-icon-72x72.png', 'apple-touch-icon.png', 'mstile-70x70.png', - 'android-chrome-36x36.png', 'apple-touch-icon-precomposed.png', 'apple-touch-icon-76x76.png', - 'apple-touch-icon-60x60.png', 'android-chrome-96x96.png', 'mstile-144x144.png', 'mstile-150x150.png', - 'safari-pinned-tab.svg', 'android-chrome-144x144.png', 'apple-touch-icon-152x152.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', 'reload.png'] +favicon_paths = [ + "apple-touch-icon-180x180.png", + "apple-touch-icon-114x114.png", + "android-chrome-72x72.png", + "apple-touch-icon-57x57.png", + "apple-touch-icon-72x72.png", + "apple-touch-icon.png", + "mstile-70x70.png", + "android-chrome-36x36.png", + "apple-touch-icon-precomposed.png", + "apple-touch-icon-76x76.png", + "apple-touch-icon-60x60.png", + "android-chrome-96x96.png", + "mstile-144x144.png", + "mstile-150x150.png", + "safari-pinned-tab.svg", + "android-chrome-144x144.png", + "apple-touch-icon-152x152.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", + "reload.png", +] for favicon in favicon_paths: - urlpatterns.append(url(r'^%s$' % favicon, RedirectView.as_view( - url=static('icons/' + favicon) - ))) + urlpatterns.append( + url(r"^%s$" % favicon, RedirectView.as_view(url=static("icons/" + favicon))) + ) -handler404 = 'judge.views.error.error404' -handler403 = 'judge.views.error.error403' -handler500 = 'judge.views.error.error500' +handler404 = "judge.views.error.error404" +handler403 = "judge.views.error.error403" +handler500 = "judge.views.error.error500" -if 'newsletter' in settings.INSTALLED_APPS: - urlpatterns.append(url(r'^newsletter/', include('newsletter.urls'))) -if 'impersonate' in settings.INSTALLED_APPS: - urlpatterns.append(url(r'^impersonate/', include('impersonate.urls'))) +if "newsletter" in settings.INSTALLED_APPS: + urlpatterns.append(url(r"^newsletter/", include("newsletter.urls"))) +if "impersonate" in settings.INSTALLED_APPS: + urlpatterns.append(url(r"^impersonate/", include("impersonate.urls"))) diff --git a/dmoj/wsgi.py b/dmoj/wsgi.py index 6bec753..3cde2a4 100644 --- a/dmoj/wsgi.py +++ b/dmoj/wsgi.py @@ -1,5 +1,6 @@ import os -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") try: import MySQLdb # noqa: F401, imported for side effect @@ -8,5 +9,8 @@ except ImportError: pymysql.install_as_MySQLdb() -from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here +from django.core.wsgi import ( + get_wsgi_application, +) # noqa: E402, django must be imported here + application = get_wsgi_application() diff --git a/dmoj/wsgi_async.py b/dmoj/wsgi_async.py index 69e105f..4a727c0 100644 --- a/dmoj/wsgi_async.py +++ b/dmoj/wsgi_async.py @@ -2,13 +2,17 @@ import os import gevent.monkey # noqa: I100, gevent must be imported here -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") gevent.monkey.patch_all() # noinspection PyUnresolvedReferences import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect -from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here +from django.core.wsgi import ( + get_wsgi_application, +) # noqa: E402, I100, I202, django must be imported here + # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect + application = get_wsgi_application() diff --git a/dmoj_bridge_async.py b/dmoj_bridge_async.py index dee4112..f514d6b 100644 --- a/dmoj_bridge_async.py +++ b/dmoj_bridge_async.py @@ -2,19 +2,22 @@ import os import gevent.monkey # noqa: I100, gevent must be imported here -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") gevent.monkey.patch_all() # noinspection PyUnresolvedReferences import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect import django # noqa: E402, F401, I100, I202, django must be imported here + django.setup() # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect -from judge.bridge.daemon import judge_daemon # noqa: E402, I100, I202, django code must be imported here +from judge.bridge.daemon import ( + judge_daemon, +) # noqa: E402, I100, I202, django code must be imported here -if __name__ == '__main__': +if __name__ == "__main__": judge_daemon() diff --git a/dmoj_celery.py b/dmoj_celery.py index 58ea344..dc7996d 100644 --- a/dmoj_celery.py +++ b/dmoj_celery.py @@ -6,7 +6,7 @@ except ImportError: import dmoj_install_pymysql # noqa: F401, imported for side effect # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect diff --git a/judge/__init__.py b/judge/__init__.py index 5c386cd..8aefa9d 100644 --- a/judge/__init__.py +++ b/judge/__init__.py @@ -1 +1 @@ -default_app_config = 'judge.apps.JudgeAppConfig' +default_app_config = "judge.apps.JudgeAppConfig" diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index c2e12f9..c9ad2f3 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry from judge.admin.comments import CommentAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin -from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin +from judge.admin.interface import ( + BlogPostAdmin, + LicenseAdmin, + LogEntryAdmin, + NavigationBarAdmin, +) from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.profile import ProfileAdmin @@ -12,10 +17,29 @@ from judge.admin.submission import SubmissionAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin from judge.admin.volunteer import VolunteerProblemVoteAdmin -from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ - ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ - OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket, \ - VolunteerProblemVote +from judge.models import ( + BlogPost, + Comment, + CommentLock, + Contest, + ContestParticipation, + ContestTag, + Judge, + Language, + License, + MiscConfig, + NavigationBar, + Organization, + OrganizationRequest, + Problem, + ProblemGroup, + ProblemPointsVote, + ProblemType, + Profile, + Submission, + Ticket, + VolunteerProblemVote, +) admin.site.register(BlogPost, BlogPostAdmin) @@ -39,4 +63,4 @@ admin.site.register(ProblemType, ProblemTypeAdmin) admin.site.register(Profile, ProfileAdmin) admin.site.register(Submission, SubmissionAdmin) admin.site.register(Ticket, TicketAdmin) -admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) \ No newline at end of file +admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) diff --git a/judge/admin/comments.py b/judge/admin/comments.py index f71de4b..b9f63d6 100644 --- a/judge/admin/comments.py +++ b/judge/admin/comments.py @@ -11,53 +11,70 @@ from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidg class CommentForm(ModelForm): class Meta: widgets = { - 'author': AdminHeavySelect2Widget(data_view='profile_select2'), - 'parent': AdminHeavySelect2Widget(data_view='comment_select2'), + "author": AdminHeavySelect2Widget(data_view="profile_select2"), + "parent": AdminHeavySelect2Widget(data_view="comment_select2"), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview')) + widgets["body"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("comment_preview") + ) class CommentAdmin(VersionAdmin): fieldsets = ( - (None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}), - ('Content', {'fields': ('body',)}), + (None, {"fields": ("author", "page", "parent", "score", "hidden")}), + ("Content", {"fields": ("body",)}), ) - list_display = ['author', 'linked_page', 'time'] - search_fields = ['author__user__username', 'page', 'body'] - readonly_fields = ['score'] - actions = ['hide_comment', 'unhide_comment'] - list_filter = ['hidden'] + list_display = ["author", "linked_page", "time"] + search_fields = ["author__user__username", "page", "body"] + readonly_fields = ["score"] + actions = ["hide_comment", "unhide_comment"] + list_filter = ["hidden"] actions_on_top = True actions_on_bottom = True form = CommentForm - date_hierarchy = 'time' + date_hierarchy = "time" def get_queryset(self, request): - return Comment.objects.order_by('-time') + return Comment.objects.order_by("-time") def hide_comment(self, request, queryset): count = queryset.update(hidden=True) - self.message_user(request, ungettext('%d comment successfully hidden.', - '%d comments successfully hidden.', - count) % count) - hide_comment.short_description = _('Hide comments') + self.message_user( + request, + ungettext( + "%d comment successfully hidden.", + "%d comments successfully hidden.", + count, + ) + % count, + ) + + hide_comment.short_description = _("Hide comments") def unhide_comment(self, request, queryset): count = queryset.update(hidden=False) - self.message_user(request, ungettext('%d comment successfully unhidden.', - '%d comments successfully unhidden.', - count) % count) - unhide_comment.short_description = _('Unhide comments') + self.message_user( + request, + ungettext( + "%d comment successfully unhidden.", + "%d comments successfully unhidden.", + count, + ) + % count, + ) + + unhide_comment.short_description = _("Unhide comments") def linked_page(self, obj): link = obj.link if link is not None: return format_html('{1}', link, obj.page) else: - return format_html('{0}', obj.page) - linked_page.short_description = _('Associated page') - linked_page.admin_order_field = 'page' + return format_html("{0}", obj.page) + + linked_page.short_description = _("Associated page") + linked_page.admin_order_field = "page" def save_model(self, request, obj, form, change): super(CommentAdmin, self).save_model(request, obj, form, change) diff --git a/judge/admin/contest.py b/judge/admin/contest.py index bda5156..c9581fb 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -16,8 +16,14 @@ from reversion_compare.admin import CompareVersionAdmin 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, \ - AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + AdminPagedownWidget, + AdminSelect2MultipleWidget, + AdminSelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class AdminHeavySelect2Widget(AdminHeavySelect2Widget): @@ -28,159 +34,252 @@ class AdminHeavySelect2Widget(AdminHeavySelect2Widget): class ContestTagForm(ModelForm): contests = ModelMultipleChoiceField( - label=_('Included contests'), + label=_("Included contests"), queryset=Contest.objects.all(), required=False, - widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2')) + widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"), + ) class ContestTagAdmin(admin.ModelAdmin): - fields = ('name', 'color', 'description', 'contests') - list_display = ('name', 'color') + fields = ("name", "color", "description", "contests") + list_display = ("name", "color") actions_on_top = True actions_on_bottom = True form = ContestTagForm if AdminPagedownWidget is not None: formfield_overrides = { - TextField: {'widget': AdminPagedownWidget}, + TextField: {"widget": AdminPagedownWidget}, } def save_model(self, request, obj, form, change): super(ContestTagAdmin, self).save_model(request, obj, form, change) - obj.contests.set(form.cleaned_data['contests']) + obj.contests.set(form.cleaned_data["contests"]) def get_form(self, request, obj=None, **kwargs): form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs) if obj is not None: - form.base_fields['contests'].initial = obj.contests.all() + form.base_fields["contests"].initial = obj.contests.all() return form class ContestProblemInlineForm(ModelForm): class Meta: - widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')} + widgets = {"problem": AdminHeavySelect2Widget(data_view="problem_select2")} class ContestProblemInline(admin.TabularInline): model = ContestProblem - verbose_name = _('Problem') - verbose_name_plural = 'Problems' - fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order', - 'rejudge_column') - readonly_fields = ('rejudge_column',) + verbose_name = _("Problem") + verbose_name_plural = "Problems" + fields = ( + "problem", + "points", + "partial", + "is_pretested", + "max_submissions", + "output_prefix_override", + "order", + "rejudge_column", + ) + readonly_fields = ("rejudge_column",) form = ContestProblemInlineForm def rejudge_column(self, obj): if obj.id is None: - return '' - return format_html('Rejudge', - reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id))) - rejudge_column.short_description = '' + return "" + return format_html( + 'Rejudge', + reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)), + ) + + rejudge_column.short_description = "" class ContestForm(ModelForm): def __init__(self, *args, **kwargs): super(ContestForm, self).__init__(*args, **kwargs) - if 'rate_exclude' in self.fields: + if "rate_exclude" in self.fields: if self.instance and self.instance.id: - self.fields['rate_exclude'].queryset = \ - Profile.objects.filter(contest_history__contest=self.instance).distinct() + self.fields["rate_exclude"].queryset = Profile.objects.filter( + contest_history__contest=self.instance + ).distinct() else: - self.fields['rate_exclude'].queryset = Profile.objects.none() - self.fields['banned_users'].widget.can_add_related = False - self.fields['view_contest_scoreboard'].widget.can_add_related = False + self.fields["rate_exclude"].queryset = Profile.objects.none() + self.fields["banned_users"].widget.can_add_related = False + self.fields["view_contest_scoreboard"].widget.can_add_related = False def clean(self): cleaned_data = super(ContestForm, self).clean() - cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None) + cleaned_data["banned_users"].filter( + current_contest__contest=self.instance + ).update(current_contest=None) class Meta: widgets = { - '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'), - 'tags': AdminSelect2MultipleWidget, - 'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'view_contest_scoreboard': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), + "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" + ), + "tags": AdminSelect2MultipleWidget, + "banned_users": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "view_contest_scoreboard": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview')) + widgets["description"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("contest_preview") + ) class ContestAdmin(CompareVersionAdmin): fieldsets = ( - (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', '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',)}), + (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", "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') + 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 form = ContestForm - change_list_template = 'admin/judge/contest/change_list.html' - filter_horizontal = ['rate_exclude'] - date_hierarchy = 'start_time' + change_list_template = "admin/judge/contest/change_list.html" + filter_horizontal = ["rate_exclude"] + date_hierarchy = "start_time" def get_actions(self, request): actions = super(ContestAdmin, self).get_actions(request) - if request.user.has_perm('judge.change_contest_visibility') or \ - request.user.has_perm('judge.create_private_contest'): - for action in ('make_visible', 'make_hidden'): + if request.user.has_perm( + "judge.change_contest_visibility" + ) or request.user.has_perm("judge.create_private_contest"): + for action in ("make_visible", "make_hidden"): actions[action] = self.get_action(action) return actions def get_queryset(self, request): queryset = Contest.objects.all() - if request.user.has_perm('judge.edit_all_contest'): + if request.user.has_perm("judge.edit_all_contest"): return queryset else: - return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct() + return queryset.filter( + Q(authors=request.profile) | Q(curators=request.profile) + ).distinct() def get_readonly_fields(self, request, obj=None): readonly = [] - if not request.user.has_perm('judge.contest_rating'): - readonly += ['is_rated', 'rate_all', 'rate_exclude'] - if not request.user.has_perm('judge.contest_access_code'): - readonly += ['access_code'] - if not request.user.has_perm('judge.create_private_contest'): - 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'] + if not request.user.has_perm("judge.contest_rating"): + readonly += ["is_rated", "rate_all", "rate_exclude"] + if not request.user.has_perm("judge.contest_access_code"): + readonly += ["access_code"] + if not request.user.has_perm("judge.create_private_contest"): + 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): # `is_visible` will not appear in `cleaned_data` if user cannot edit it - if form.cleaned_data.get('is_visible') and not request.user.has_perm('judge.change_contest_visibility'): - if not form.cleaned_data['is_private'] and not form.cleaned_data['is_organization_private']: + if form.cleaned_data.get("is_visible") and not request.user.has_perm( + "judge.change_contest_visibility" + ): + if ( + not form.cleaned_data["is_private"] + and not form.cleaned_data["is_organization_private"] + ): raise PermissionDenied - if not request.user.has_perm('judge.create_private_contest'): + if not request.user.has_perm("judge.create_private_contest"): raise PermissionDenied super().save_model(request, obj, form, change) # We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored self._rescored = False - if form.changed_data and any(f in form.changed_data for f in ('format_config', 'format_name')): + if form.changed_data and any( + f in form.changed_data for f in ("format_config", "format_name") + ): self._rescore(obj.key) self._rescored = True @@ -188,10 +287,10 @@ class ContestAdmin(CompareVersionAdmin): super().save_related(request, form, formsets, change) # Only rescored if we did not already do so in `save_model` if not self._rescored and any(formset.has_changed() for formset in formsets): - self._rescore(form.cleaned_data['key']) + self._rescore(form.cleaned_data["key"]) def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.edit_own_contest'): + if not request.user.has_perm("judge.edit_own_contest"): return False if obj is None: return True @@ -199,76 +298,113 @@ class ContestAdmin(CompareVersionAdmin): def _rescore(self, contest_key): from judge.tasks import rescore_contest + transaction.on_commit(rescore_contest.s(contest_key).delay) def make_visible(self, request, queryset): - if not request.user.has_perm('judge.change_contest_visibility'): - queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) + if not request.user.has_perm("judge.change_contest_visibility"): + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, ungettext('%d contest successfully marked as visible.', - '%d contests successfully marked as visible.', - count) % count) - make_visible.short_description = _('Mark contests as visible') + self.message_user( + request, + ungettext( + "%d contest successfully marked as visible.", + "%d contests successfully marked as visible.", + count, + ) + % count, + ) + + make_visible.short_description = _("Mark contests as visible") def make_hidden(self, request, queryset): - if not request.user.has_perm('judge.change_contest_visibility'): - queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) + if not request.user.has_perm("judge.change_contest_visibility"): + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, ungettext('%d contest successfully marked as hidden.', - '%d contests successfully marked as hidden.', - count) % count) - make_hidden.short_description = _('Mark contests as hidden') + self.message_user( + request, + ungettext( + "%d contest successfully marked as hidden.", + "%d contests successfully marked as hidden.", + count, + ) + % count, + ) + + make_hidden.short_description = _("Mark contests as hidden") def get_urls(self): return [ - url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'), - url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'), - url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'), + url(r"^rate/all/$", self.rate_all_view, name="judge_contest_rate_all"), + url(r"^(\d+)/rate/$", self.rate_view, name="judge_contest_rate"), + url( + r"^(\d+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge" + ), ] + super(ContestAdmin, self).get_urls() def rejudge_view(self, request, contest_id, problem_id): - queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission') + queryset = ContestSubmission.objects.filter( + problem_id=problem_id + ).select_related("submission") for model in queryset: model.submission.judge(rejudge=True) - self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.', - '%d submissions were successfully scheduled for rejudging.', - len(queryset)) % len(queryset)) - return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,))) + self.message_user( + request, + ungettext( + "%d submission was successfully scheduled for rejudging.", + "%d submissions were successfully scheduled for rejudging.", + len(queryset), + ) + % len(queryset), + ) + return HttpResponseRedirect( + reverse("admin:judge_contest_change", args=(contest_id,)) + ) def rate_all_view(self, request): - if not request.user.has_perm('judge.contest_rating'): + if not request.user.has_perm("judge.contest_rating"): raise PermissionDenied() with transaction.atomic(): with connection.cursor() as cursor: - cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) + 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'): + 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')) + return HttpResponseRedirect(reverse("admin:judge_contest_changelist")) def rate_view(self, request, id): - if not request.user.has_perm('judge.contest_rating'): + if not request.user.has_perm("judge.contest_rating"): raise PermissionDenied() contest = get_object_or_404(Contest, id=id) 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'))) + return HttpResponseRedirect( + request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist")) + ) 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: + 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['curators'].queryset = Profile.objects.filter( - Q(user__is_superuser=True) | - Q(user__groups__permissions__codename__in=perms) | - Q(user__user_permissions__codename__in=perms), + form.base_fields["problem_label_script"].widget = AceWidget( + "lua", request.profile.ace_theme + ) + + perms = ("edit_own_contest", "edit_all_contest") + 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), ).distinct() return form @@ -276,29 +412,48 @@ class ContestAdmin(CompareVersionAdmin): class ContestParticipationForm(ModelForm): class Meta: widgets = { - 'contest': AdminSelect2Widget(), - 'user': AdminHeavySelect2Widget(data_view='profile_select2'), + "contest": AdminSelect2Widget(), + "user": AdminHeavySelect2Widget(data_view="profile_select2"), } class ContestParticipationAdmin(admin.ModelAdmin): - fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') - list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker') - actions = ['recalculate_results'] + fields = ("contest", "user", "real_start", "virtual", "is_disqualified") + 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') + search_fields = ("contest__key", "contest__name", "user__user__username") form = ContestParticipationForm - date_hierarchy = 'real_start' + date_hierarchy = "real_start" 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', 'tiebreaker', 'virtual', + return ( + super(ContestParticipationAdmin, self) + .get_queryset(request) + .only( + "contest__name", + "contest__format_name", + "contest__format_config", + "user__user__username", + "real_start", + "score", + "cumtime", + "tiebreaker", + "virtual", + ) ) def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) - if form.changed_data and 'is_disqualified' in form.changed_data: + if form.changed_data and "is_disqualified" in form.changed_data: obj.set_disqualified(obj.is_disqualified) def recalculate_results(self, request, queryset): @@ -306,17 +461,26 @@ class ContestParticipationAdmin(admin.ModelAdmin): for participation in queryset: participation.recompute_results() count += 1 - self.message_user(request, ungettext('%d participation recalculated.', - '%d participations recalculated.', - count) % count) - recalculate_results.short_description = _('Recalculate results') + self.message_user( + request, + ungettext( + "%d participation recalculated.", + "%d participations recalculated.", + count, + ) + % count, + ) + + recalculate_results.short_description = _("Recalculate results") def username(self, obj): return obj.user.username - username.short_description = _('username') - username.admin_order_field = 'user__user__username' + + username.short_description = _("username") + username.admin_order_field = "user__user__username" def show_virtual(self, obj): - return obj.virtual or '-' - show_virtual.short_description = _('virtual') - show_virtual.admin_order_field = 'virtual' + return obj.virtual or "-" + + show_virtual.short_description = _("virtual") + show_virtual.admin_order_field = "virtual" diff --git a/judge/admin/interface.py b/judge/admin/interface.py index fb57844..5ee4c38 100644 --- a/judge/admin/interface.py +++ b/judge/admin/interface.py @@ -11,23 +11,28 @@ from reversion_compare.admin import CompareVersionAdmin from judge.dblock import LockModel from judge.models import NavigationBar -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class NavigationBarAdmin(DraggableMPTTAdmin): - list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path') - fields = ('key', 'label', 'path', 'order', 'regex', 'parent') + list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path") + fields = ("key", "label", "path", "order", "regex", "parent") list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set mptt_level_indent = 20 - sortable = 'order' + sortable = "order" def __init__(self, *args, **kwargs): super(NavigationBarAdmin, self).__init__(*args, **kwargs) self.__save_model_calls = 0 def linked_path(self, obj): - return format_html(u'{0}', obj.path) - linked_path.short_description = _('link path') + return format_html('{0}', obj.path) + + linked_path.short_description = _("link path") def save_model(self, request, obj, form, change): self.__save_model_calls += 1 @@ -36,7 +41,9 @@ class NavigationBarAdmin(DraggableMPTTAdmin): def changelist_view(self, request, extra_context=None): self.__save_model_calls = 0 with NavigationBar.objects.disable_mptt_updates(): - result = super(NavigationBarAdmin, self).changelist_view(request, extra_context) + result = super(NavigationBarAdmin, self).changelist_view( + request, extra_context + ) if self.__save_model_calls: with LockModel(write=(NavigationBar,)): NavigationBar.objects.rebuild() @@ -46,74 +53,105 @@ class NavigationBarAdmin(DraggableMPTTAdmin): class BlogPostForm(ModelForm): def __init__(self, *args, **kwargs): super(BlogPostForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', - attrs={'style': 'width: 100%'}), + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "organizations": AdminHeavySelect2MultipleWidget( + data_view="organization_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview')) - widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview')) + widgets["content"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("blog_preview") + ) + widgets["summary"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("blog_preview") + ) class BlogPostAdmin(CompareVersionAdmin): fieldsets = ( - (None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on', - 'is_organization_private', 'organizations')}), - (_('Content'), {'fields': ('content', 'og_image')}), - (_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}), + ( + None, + { + "fields": ( + "title", + "slug", + "authors", + "visible", + "sticky", + "publish_on", + "is_organization_private", + "organizations", + ) + }, + ), + (_("Content"), {"fields": ("content", "og_image")}), + (_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}), ) - prepopulated_fields = {'slug': ('title',)} - list_display = ('id', 'title', 'visible', 'sticky', 'publish_on') - list_display_links = ('id', 'title') - ordering = ('-publish_on',) + prepopulated_fields = {"slug": ("title",)} + list_display = ("id", "title", "visible", "sticky", "publish_on") + list_display_links = ("id", "title") + ordering = ("-publish_on",) form = BlogPostForm - date_hierarchy = 'publish_on' + date_hierarchy = "publish_on" def has_change_permission(self, request, obj=None): - return (request.user.has_perm('judge.edit_all_post') or - request.user.has_perm('judge.change_blogpost') and ( - obj is None or - obj.authors.filter(id=request.profile.id).exists())) + return ( + request.user.has_perm("judge.edit_all_post") + or request.user.has_perm("judge.change_blogpost") + and (obj is None or obj.authors.filter(id=request.profile.id).exists()) + ) class SolutionForm(ModelForm): def __init__(self, *args, **kwargs): super(SolutionForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}), + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "problem": AdminHeavySelect2Widget( + data_view="problem_select2", attrs={"style": "width: 250px"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview')) + widgets["content"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("solution_preview") + ) class LicenseForm(ModelForm): class Meta: if HeavyPreviewAdminPageDownWidget is not None: - widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))} + widgets = { + "text": HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("license_preview") + ) + } class LicenseAdmin(admin.ModelAdmin): - fields = ('key', 'link', 'name', 'display', 'icon', 'text') - list_display = ('name', 'key') + fields = ("key", "link", "name", "display", "icon", "text") + list_display = ("name", "key") form = LicenseForm class UserListFilter(admin.SimpleListFilter): - title = _('user') - parameter_name = 'user' + title = _("user") + parameter_name = "user" def lookups(self, request, model_admin): - return User.objects.filter(is_staff=True).values_list('id', 'username') + return User.objects.filter(is_staff=True).values_list("id", "username") def queryset(self, request, queryset): if self.value(): @@ -122,10 +160,24 @@ class UserListFilter(admin.SimpleListFilter): class LogEntryAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message') - list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link', 'diff_link') - search_fields = ('object_repr', 'change_message') - list_filter = (UserListFilter, 'content_type') + readonly_fields = ( + "user", + "content_type", + "object_id", + "object_repr", + "action_flag", + "change_message", + ) + list_display = ( + "__str__", + "action_time", + "user", + "content_type", + "object_link", + "diff_link", + ) + search_fields = ("object_repr", "change_message") + list_filter = (UserListFilter, "content_type") list_display_links = None actions = None @@ -144,25 +196,35 @@ class LogEntryAdmin(admin.ModelAdmin): else: ct = obj.content_type try: - link = format_html('{0}', obj.object_repr, - reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,))) + link = format_html( + '{0}', + obj.object_repr, + reverse( + "admin:%s_%s_change" % (ct.app_label, ct.model), + args=(obj.object_id,), + ), + ) except NoReverseMatch: link = obj.object_repr return link - object_link.admin_order_field = 'object_repr' - object_link.short_description = _('object') + + object_link.admin_order_field = "object_repr" + object_link.short_description = _("object") def diff_link(self, obj): if obj.is_deletion(): return None ct = obj.content_type try: - url = reverse('admin:%s_%s_history' % (ct.app_label, ct.model), args=(obj.object_id,)) - link = format_html('{0}', _('Diff'), url) + url = reverse( + "admin:%s_%s_history" % (ct.app_label, ct.model), args=(obj.object_id,) + ) + link = format_html('{0}', _("Diff"), url) except NoReverseMatch: link = None return link - diff_link.short_description = _('diff') + + diff_link.short_description = _("diff") def queryset(self, request): - return super().queryset(request).prefetch_related('content_type') + return super().queryset(request).prefetch_related("content_type") diff --git a/judge/admin/organization.py b/judge/admin/organization.py index f9d78d4..64a4da0 100644 --- a/judge/admin/organization.py +++ b/judge/admin/organization.py @@ -6,61 +6,88 @@ from django.utils.translation import gettext, gettext_lazy as _ from reversion.admin import VersionAdmin from judge.models import Organization -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class OrganizationForm(ModelForm): class Meta: widgets = { - 'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'registrant': AdminHeavySelect2Widget(data_view='profile_select2'), + "admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "registrant": AdminHeavySelect2Widget(data_view="profile_select2"), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview')) + widgets["about"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("organization_preview") + ) class OrganizationAdmin(VersionAdmin): - readonly_fields = ('creation_date',) - fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant', - 'creation_date', 'admins') - list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public') - prepopulated_fields = {'slug': ('name',)} + readonly_fields = ("creation_date",) + fields = ( + "name", + "slug", + "short_name", + "is_open", + "about", + "logo_override_image", + "slots", + "registrant", + "creation_date", + "admins", + ) + list_display = ( + "name", + "short_name", + "is_open", + "slots", + "registrant", + "show_public", + ) + prepopulated_fields = {"slug": ("name",)} actions_on_top = True actions_on_bottom = True form = OrganizationForm def show_public(self, obj): - return format_html('{1}', - obj.get_absolute_url(), gettext('View on site')) + return format_html( + '{1}', + obj.get_absolute_url(), + gettext("View on site"), + ) - show_public.short_description = '' + show_public.short_description = "" def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.organization_admin'): - return fields + ('registrant', 'admins', 'is_open', 'slots') + if not request.user.has_perm("judge.organization_admin"): + return fields + ("registrant", "admins", "is_open", "slots") return fields def get_queryset(self, request): queryset = Organization.objects.all() - if request.user.has_perm('judge.edit_all_organization'): + if request.user.has_perm("judge.edit_all_organization"): return queryset else: return queryset.filter(admins=request.profile.id) def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.change_organization'): + if not request.user.has_perm("judge.change_organization"): return False - if request.user.has_perm('judge.edit_all_organization') or obj is None: + if request.user.has_perm("judge.edit_all_organization") or obj is None: return True return obj.admins.filter(id=request.profile.id).exists() class OrganizationRequestAdmin(admin.ModelAdmin): - list_display = ('username', 'organization', 'state', 'time') - readonly_fields = ('user', 'organization') + list_display = ("username", "organization", "state", "time") + readonly_fields = ("user", "organization") def username(self, obj): return obj.user.user.username - username.short_description = _('username') - username.admin_order_field = 'user__user__username' + + username.short_description = _("username") + username.admin_order_field = "user__user__username" diff --git a/judge/admin/problem.py b/judge/admin/problem.py index a8e6c35..c687655 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -14,45 +14,74 @@ from reversion.admin import VersionAdmin from reversion_compare.admin import CompareVersionAdmin -from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \ - CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget +from judge.models import ( + LanguageLimit, + Problem, + ProblemClarification, + ProblemTranslation, + Profile, + Solution, +) +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminSelect2MultipleWidget, + AdminSelect2Widget, + CheckboxSelectMultipleWithSelectAll, + HeavyPreviewAdminPageDownWidget, + HeavyPreviewPageDownWidget, +) class ProblemForm(ModelForm): - change_message = forms.CharField(max_length=256, label='Edit reason', required=False) + change_message = forms.CharField( + max_length=256, label="Edit reason", required=False + ) def __init__(self, *args, **kwargs): super(ProblemForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False - self.fields['curators'].widget.can_add_related = False - self.fields['testers'].widget.can_add_related = False - self.fields['banned_users'].widget.can_add_related = False - self.fields['change_message'].widget.attrs.update({ - 'placeholder': gettext('Describe the changes you made (optional)'), - }) + self.fields["authors"].widget.can_add_related = False + self.fields["curators"].widget.can_add_related = False + self.fields["testers"].widget.can_add_related = False + self.fields["banned_users"].widget.can_add_related = False + self.fields["change_message"].widget.attrs.update( + { + "placeholder": gettext("Describe the changes you made (optional)"), + } + ) class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', - attrs={'style': 'width: 100%'}), - 'types': AdminSelect2MultipleWidget, - 'group': AdminSelect2Widget, + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "curators": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "testers": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "banned_users": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "organizations": AdminHeavySelect2MultipleWidget( + data_view="organization_select2", attrs={"style": "width: 100%"} + ), + "types": AdminSelect2MultipleWidget, + "group": AdminSelect2Widget, } if HeavyPreviewAdminPageDownWidget is not None: - widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview')) + widgets["description"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("problem_preview") + ) class ProblemCreatorListFilter(admin.SimpleListFilter): - title = parameter_name = 'creator' + title = parameter_name = "creator" def lookups(self, request, model_admin): - queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True) + queryset = Profile.objects.exclude(authored_problems=None).values_list( + "user__username", flat=True + ) return [(name, name) for name in queryset] def queryset(self, request, queryset): @@ -63,24 +92,28 @@ class ProblemCreatorListFilter(admin.SimpleListFilter): class LanguageLimitInlineForm(ModelForm): class Meta: - widgets = {'language': AdminSelect2Widget} + widgets = {"language": AdminSelect2Widget} class LanguageLimitInline(admin.TabularInline): model = LanguageLimit - fields = ('language', 'time_limit', 'memory_limit') + fields = ("language", "time_limit", "memory_limit") form = LanguageLimitInlineForm class ProblemClarificationForm(ModelForm): class Meta: if HeavyPreviewPageDownWidget is not None: - widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))} + widgets = { + "description": HeavyPreviewPageDownWidget( + preview=reverse_lazy("comment_preview") + ) + } class ProblemClarificationInline(admin.StackedInline): model = ProblemClarification - fields = ('description',) + fields = ("description",) form = ProblemClarificationForm extra = 0 @@ -88,20 +121,24 @@ class ProblemClarificationInline(admin.StackedInline): class ProblemSolutionForm(ModelForm): def __init__(self, *args, **kwargs): super(ProblemSolutionForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + "authors": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview')) + widgets["content"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("solution_preview") + ) class ProblemSolutionInline(admin.StackedInline): model = Solution - fields = ('is_public', 'publish_on', 'authors', 'content') + fields = ("is_public", "publish_on", "authors", "content") form = ProblemSolutionForm extra = 0 @@ -109,168 +146,250 @@ class ProblemSolutionInline(admin.StackedInline): class ProblemTranslationForm(ModelForm): class Meta: if HeavyPreviewAdminPageDownWidget is not None: - widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))} + widgets = { + "description": HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("problem_preview") + ) + } class ProblemTranslationInline(admin.StackedInline): model = ProblemTranslation - fields = ('language', 'name', 'description') + fields = ("language", "name", "description") form = ProblemTranslationForm extra = 0 class ProblemAdmin(CompareVersionAdmin): fieldsets = ( - (None, { - 'fields': ( - 'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers', - 'is_organization_private', 'organizations', 'description', 'license', - ), - }), - (_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}), - (_('Taxonomy'), {'fields': ('types', 'group')}), - (_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}), - (_('Limits'), {'fields': ('time_limit', 'memory_limit')}), - (_('Language'), {'fields': ('allowed_languages',)}), - (_('Justice'), {'fields': ('banned_users',)}), - (_('History'), {'fields': ('change_message',)}), + ( + None, + { + "fields": ( + "code", + "name", + "is_public", + "is_manually_managed", + "date", + "authors", + "curators", + "testers", + "is_organization_private", + "organizations", + "description", + "license", + ), + }, + ), + ( + _("Social Media"), + {"classes": ("collapse",), "fields": ("og_image", "summary")}, + ), + (_("Taxonomy"), {"fields": ("types", "group")}), + (_("Points"), {"fields": (("points", "partial"), "short_circuit")}), + (_("Limits"), {"fields": ("time_limit", "memory_limit")}), + (_("Language"), {"fields": ("allowed_languages",)}), + (_("Justice"), {"fields": ("banned_users",)}), + (_("History"), {"fields": ("change_message",)}), ) - list_display = ['code', 'name', 'show_authors', 'points', 'vote_cnt', 'vote_mean', 'vote_median', 'vote_std', 'is_public', 'show_public'] - ordering = ['code'] - search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username') - inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline] + list_display = [ + "code", + "name", + "show_authors", + "points", + "vote_cnt", + "vote_mean", + "vote_median", + "vote_std", + "is_public", + "show_public", + ] + ordering = ["code"] + search_fields = ( + "code", + "name", + "authors__user__username", + "curators__user__username", + ) + inlines = [ + LanguageLimitInline, + ProblemClarificationInline, + ProblemSolutionInline, + ProblemTranslationInline, + ] list_max_show_all = 1000 actions_on_top = True actions_on_bottom = True - list_filter = ('is_public', ProblemCreatorListFilter) + list_filter = ("is_public", ProblemCreatorListFilter) form = ProblemForm - date_hierarchy = 'date' + date_hierarchy = "date" def get_actions(self, request): actions = super(ProblemAdmin, self).get_actions(request) - if request.user.has_perm('judge.change_public_visibility'): - func, name, desc = self.get_action('make_public') + if request.user.has_perm("judge.change_public_visibility"): + func, name, desc = self.get_action("make_public") actions[name] = (func, name, desc) - func, name, desc = self.get_action('make_private') + func, name, desc = self.get_action("make_private") actions[name] = (func, name, desc) return actions def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.change_public_visibility'): - fields += ('is_public',) - if not request.user.has_perm('judge.change_manually_managed'): - fields += ('is_manually_managed',) + if not request.user.has_perm("judge.change_public_visibility"): + fields += ("is_public",) + if not request.user.has_perm("judge.change_manually_managed"): + fields += ("is_manually_managed",) return fields def show_authors(self, obj): - return ', '.join(map(attrgetter('user.username'), obj.authors.all())) + return ", ".join(map(attrgetter("user.username"), obj.authors.all())) - show_authors.short_description = _('Authors') + show_authors.short_description = _("Authors") def show_public(self, obj): - return format_html('{0}', gettext('View on site'), obj.get_absolute_url()) + return format_html( + '{0}', gettext("View on site"), obj.get_absolute_url() + ) - show_public.short_description = '' + show_public.short_description = "" def _rescore(self, request, problem_id): from judge.tasks import rescore_problem + transaction.on_commit(rescore_problem.s(problem_id).delay) def make_public(self, request, queryset): count = queryset.update(is_public=True) - for problem_id in queryset.values_list('id', flat=True): + for problem_id in queryset.values_list("id", flat=True): self._rescore(request, problem_id) - self.message_user(request, ungettext('%d problem successfully marked as public.', - '%d problems successfully marked as public.', - count) % count) + self.message_user( + request, + ungettext( + "%d problem successfully marked as public.", + "%d problems successfully marked as public.", + count, + ) + % count, + ) - make_public.short_description = _('Mark problems as public') + make_public.short_description = _("Mark problems as public") def make_private(self, request, queryset): count = queryset.update(is_public=False) - for problem_id in queryset.values_list('id', flat=True): + for problem_id in queryset.values_list("id", flat=True): self._rescore(request, problem_id) - self.message_user(request, ungettext('%d problem successfully marked as private.', - '%d problems successfully marked as private.', - count) % count) + self.message_user( + request, + ungettext( + "%d problem successfully marked as private.", + "%d problems successfully marked as private.", + count, + ) + % count, + ) - make_private.short_description = _('Mark problems as private') + make_private.short_description = _("Mark problems as private") def get_queryset(self, request): - queryset = Problem.objects.prefetch_related('authors__user') + queryset = Problem.objects.prefetch_related("authors__user") queryset = queryset.annotate( - _vote_mean=Avg('problem_points_votes__points'), - _vote_std=StdDev('problem_points_votes__points'), - _vote_cnt=Count('problem_points_votes__points') + _vote_mean=Avg("problem_points_votes__points"), + _vote_std=StdDev("problem_points_votes__points"), + _vote_cnt=Count("problem_points_votes__points"), ) - if request.user.has_perm('judge.edit_all_problem'): + if request.user.has_perm("judge.edit_all_problem"): return queryset access = Q() - if request.user.has_perm('judge.edit_public_problem'): + if request.user.has_perm("judge.edit_public_problem"): access |= Q(is_public=True) - if request.user.has_perm('judge.edit_own_problem'): - access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id) + if request.user.has_perm("judge.edit_own_problem"): + access |= Q(authors__id=request.profile.id) | Q( + curators__id=request.profile.id + ) return queryset.filter(access).distinct() if access else queryset.none() def has_change_permission(self, request, obj=None): - if request.user.has_perm('judge.edit_all_problem') or obj is None: + if request.user.has_perm("judge.edit_all_problem") or obj is None: return True - if request.user.has_perm('judge.edit_public_problem') and obj.is_public: + if request.user.has_perm("judge.edit_public_problem") and obj.is_public: return True - if not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm("judge.edit_own_problem"): return False return obj.is_editor(request.profile) def formfield_for_manytomany(self, db_field, request=None, **kwargs): - if db_field.name == 'allowed_languages': - kwargs['widget'] = CheckboxSelectMultipleWithSelectAll() - return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs) + if db_field.name == "allowed_languages": + kwargs["widget"] = CheckboxSelectMultipleWithSelectAll() + return super(ProblemAdmin, self).formfield_for_manytomany( + db_field, request, **kwargs + ) def get_form(self, *args, **kwargs): form = super(ProblemAdmin, self).get_form(*args, **kwargs) - form.base_fields['authors'].queryset = Profile.objects.all() + form.base_fields["authors"].queryset = Profile.objects.all() return form def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) - if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')): + if form.changed_data and any( + f in form.changed_data for f in ("is_public", "points", "partial") + ): self._rescore(request, obj.id) def construct_change_message(self, request, form, *args, **kwargs): - if form.cleaned_data.get('change_message'): - return form.cleaned_data['change_message'] - return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) + if form.cleaned_data.get("change_message"): + return form.cleaned_data["change_message"] + return super(ProblemAdmin, self).construct_change_message( + request, form, *args, **kwargs + ) def vote_mean(self, obj): return round(obj._vote_mean, 1) if obj._vote_mean is not None else None - vote_mean.admin_order_field = '_vote_mean' + + vote_mean.admin_order_field = "_vote_mean" def vote_std(self, obj): return round(obj._vote_std, 1) if obj._vote_std is not None else None - vote_std.admin_order_field = '_vote_std' + + vote_std.admin_order_field = "_vote_std" def vote_cnt(self, obj): return obj._vote_cnt - vote_cnt.admin_order_field = '_vote_cnt' + + vote_cnt.admin_order_field = "_vote_cnt" def vote_median(self, obj): - votes = obj.problem_points_votes.values_list('points', flat=True) - return statistics.median(votes) if votes else None + votes = obj.problem_points_votes.values_list("points", flat=True) + return statistics.median(votes) if votes else None class ProblemPointsVoteAdmin(admin.ModelAdmin): - list_display = ('vote_points', 'voter', 'voter_rating', 'voter_point', 'problem_name', 'problem_code', 'problem_points') - search_fields = ('voter__user__username', 'problem__code', 'problem__name') - readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'voter_rating', 'voter_point') + list_display = ( + "vote_points", + "voter", + "voter_rating", + "voter_point", + "problem_name", + "problem_code", + "problem_points", + ) + search_fields = ("voter__user__username", "problem__code", "problem__name") + readonly_fields = ( + "voter", + "problem", + "problem_code", + "problem_points", + "voter_rating", + "voter_point", + ) def has_change_permission(self, request, obj=None): if obj is None: - return request.user.has_perm('judge.edit_own_problem') + return request.user.has_perm("judge.edit_own_problem") return obj.problem.is_editable_by(request.user) def lookup_allowed(self, key, value): @@ -278,29 +397,35 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): def problem_code(self, obj): return obj.problem.code - problem_code.short_description = _('Problem code') - problem_code.admin_order_field = 'problem__code' + + problem_code.short_description = _("Problem code") + problem_code.admin_order_field = "problem__code" def problem_points(self, obj): return obj.problem.points - problem_points.short_description = _('Points') - problem_points.admin_order_field = 'problem__points' + + problem_points.short_description = _("Points") + problem_points.admin_order_field = "problem__points" def problem_name(self, obj): return obj.problem.name - problem_name.short_description = _('Problem name') - problem_name.admin_order_field = 'problem__name' + + problem_name.short_description = _("Problem name") + problem_name.admin_order_field = "problem__name" def voter_rating(self, obj): return obj.voter.rating - voter_rating.short_description = _('Voter rating') - voter_rating.admin_order_field = 'voter__rating' + + voter_rating.short_description = _("Voter rating") + voter_rating.admin_order_field = "voter__rating" def voter_point(self, obj): return round(obj.voter.performance_points) - voter_point.short_description = _('Voter point') - voter_point.admin_order_field = 'voter__performance_points' + + voter_point.short_description = _("Voter point") + voter_point.admin_order_field = "voter__performance_points" def vote_points(self, obj): return obj.points - vote_points.short_description = _('Vote') \ No newline at end of file + + vote_points.short_description = _("Vote") diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 02a1f71..d19a80b 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -12,30 +12,40 @@ from judge.widgets import AdminPagedownWidget, AdminSelect2Widget class ProfileForm(ModelForm): def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) - if 'current_contest' in self.base_fields: + if "current_contest" in self.base_fields: # form.fields['current_contest'] does not exist when the user has only view permission on the model. - self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \ - .only('contest__name', 'user_id', 'virtual') - self.fields['current_contest'].label_from_instance = \ - lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name + self.fields[ + "current_contest" + ].queryset = self.instance.contest_history.select_related("contest").only( + "contest__name", "user_id", "virtual" + ) + self.fields["current_contest"].label_from_instance = ( + lambda obj: "%s v%d" % (obj.contest.name, obj.virtual) + if obj.virtual + else obj.contest.name + ) class Meta: widgets = { - 'timezone': AdminSelect2Widget, - 'language': AdminSelect2Widget, - 'ace_theme': AdminSelect2Widget, - 'current_contest': AdminSelect2Widget, + "timezone": AdminSelect2Widget, + "language": AdminSelect2Widget, + "ace_theme": AdminSelect2Widget, + "current_contest": AdminSelect2Widget, } if AdminPagedownWidget is not None: - widgets['about'] = AdminPagedownWidget + widgets["about"] = AdminPagedownWidget class TimezoneFilter(admin.SimpleListFilter): - title = _('timezone') - parameter_name = 'timezone' + title = _("timezone") + parameter_name = "timezone" def lookups(self, request, model_admin): - return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone') + return ( + Profile.objects.values_list("timezone", "timezone") + .distinct() + .order_by("timezone") + ) def queryset(self, request, queryset): if self.value() is None: @@ -44,75 +54,116 @@ class TimezoneFilter(admin.SimpleListFilter): class ProfileAdmin(VersionAdmin): - fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', - 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'is_banned_problem_voting', 'notes', 'is_totp_enabled', 'user_script', - 'current_contest') - readonly_fields = ('user',) - list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full', - 'date_joined', 'last_access', 'ip', 'show_public') - ordering = ('user__username',) - search_fields = ('user__username', 'ip', 'user__email') - list_filter = ('language', TimezoneFilter) - actions = ('recalculate_points',) + fields = ( + "user", + "display_rank", + "about", + "organizations", + "timezone", + "language", + "ace_theme", + "math_engine", + "last_access", + "ip", + "mute", + "is_unlisted", + "is_banned_problem_voting", + "notes", + "is_totp_enabled", + "user_script", + "current_contest", + ) + readonly_fields = ("user",) + list_display = ( + "admin_user_admin", + "email", + "is_totp_enabled", + "timezone_full", + "date_joined", + "last_access", + "ip", + "show_public", + ) + ordering = ("user__username",) + search_fields = ("user__username", "ip", "user__email") + list_filter = ("language", TimezoneFilter) + actions = ("recalculate_points",) actions_on_top = True actions_on_bottom = True form = ProfileForm def get_queryset(self, request): - return super(ProfileAdmin, self).get_queryset(request).select_related('user') + return super(ProfileAdmin, self).get_queryset(request).select_related("user") def get_fields(self, request, obj=None): - if request.user.has_perm('judge.totp'): + if request.user.has_perm("judge.totp"): fields = list(self.fields) - fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key') + fields.insert(fields.index("is_totp_enabled") + 1, "totp_key") return tuple(fields) else: return self.fields def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.totp'): - fields += ('is_totp_enabled',) + if not request.user.has_perm("judge.totp"): + fields += ("is_totp_enabled",) return fields def show_public(self, obj): - return format_html('{1}', - obj.get_absolute_url(), gettext('View on site')) - show_public.short_description = '' + return format_html( + '{1}', + obj.get_absolute_url(), + gettext("View on site"), + ) + + show_public.short_description = "" def admin_user_admin(self, obj): return obj.username - admin_user_admin.admin_order_field = 'user__username' - admin_user_admin.short_description = _('User') + + admin_user_admin.admin_order_field = "user__username" + admin_user_admin.short_description = _("User") def email(self, obj): return obj.user.email - email.admin_order_field = 'user__email' - email.short_description = _('Email') + + email.admin_order_field = "user__email" + email.short_description = _("Email") def timezone_full(self, obj): return obj.timezone - timezone_full.admin_order_field = 'timezone' - timezone_full.short_description = _('Timezone') + + timezone_full.admin_order_field = "timezone" + timezone_full.short_description = _("Timezone") def date_joined(self, obj): return obj.user.date_joined - date_joined.admin_order_field = 'user__date_joined' - date_joined.short_description = _('date joined') + + date_joined.admin_order_field = "user__date_joined" + date_joined.short_description = _("date joined") def recalculate_points(self, request, queryset): count = 0 for profile in queryset: profile.calculate_points() count += 1 - self.message_user(request, ungettext('%d user have scores recalculated.', - '%d users have scores recalculated.', - count) % count) - recalculate_points.short_description = _('Recalculate scores') + self.message_user( + request, + ungettext( + "%d user have scores recalculated.", + "%d users have scores recalculated.", + count, + ) + % count, + ) + + recalculate_points.short_description = _("Recalculate scores") def get_form(self, request, obj=None, **kwargs): form = super(ProfileAdmin, self).get_form(request, obj, **kwargs) - if 'user_script' in form.base_fields: + if "user_script" in form.base_fields: # form.base_fields['user_script'] does not exist when the user has only view permission on the model. - form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme) + form.base_fields["user_script"].widget = AceWidget( + "javascript", request.profile.ace_theme + ) return form diff --git a/judge/admin/runtime.py b/judge/admin/runtime.py index e9f44ec..791083a 100644 --- a/judge/admin/runtime.py +++ b/judge/admin/runtime.py @@ -16,41 +16,63 @@ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget class LanguageForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Disallowed problems'), + label=_("Disallowed problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are NOT allowed to be submitted in this language'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are NOT allowed to be submitted in this language"), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class Meta: if AdminPagedownWidget is not None: - widgets = {'description': AdminPagedownWidget} + widgets = {"description": AdminPagedownWidget} class LanguageAdmin(VersionAdmin): - fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description', - 'template', 'problems') - list_display = ('key', 'name', 'common_name', 'info') + fields = ( + "key", + "name", + "short_name", + "common_name", + "ace", + "pygments", + "info", + "description", + "template", + "problems", + ) + list_display = ("key", "name", "common_name", "info") form = LanguageForm def save_model(self, request, obj, form, change): super(LanguageAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id'))) + obj.problem_set.set( + Problem.objects.exclude(id__in=form.cleaned_data["problems"].values("id")) + ) def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = \ - Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else [] + self.form.base_fields["problems"].initial = ( + Problem.objects.exclude(id__in=obj.problem_set.values("id")).values_list( + "pk", flat=True + ) + if obj + else [] + ) form = super(LanguageAdmin, self).get_form(request, obj, **kwargs) if obj is not None: - form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme) + form.base_fields["template"].widget = AceWidget( + obj.ace, request.profile.ace_theme + ) return form class GenerateKeyTextInput(TextInput): def render(self, name, value, attrs=None, renderer=None): text = super(TextInput, self).render(name, value, attrs) - return mark_safe(text + format_html( - '''\ + return mark_safe( + text + + format_html( + """\ Regenerate -''', name)) +""", + name, + ) + ) class JudgeAdminForm(ModelForm): class Meta: - widgets = {'auth_key': GenerateKeyTextInput} + widgets = {"auth_key": GenerateKeyTextInput} if AdminPagedownWidget is not None: - widgets['description'] = AdminPagedownWidget + widgets["description"] = AdminPagedownWidget class JudgeAdmin(VersionAdmin): form = JudgeAdminForm - readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems') - fieldsets = ( - (None, {'fields': ('name', 'auth_key', 'is_blocked')}), - (_('Description'), {'fields': ('description',)}), - (_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}), - (_('Capabilities'), {'fields': ('runtimes', 'problems')}), + readonly_fields = ( + "created", + "online", + "start_time", + "ping", + "load", + "last_ip", + "runtimes", + "problems", ) - list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip') - ordering = ['-online', 'name'] + fieldsets = ( + (None, {"fields": ("name", "auth_key", "is_blocked")}), + (_("Description"), {"fields": ("description",)}), + ( + _("Information"), + {"fields": ("created", "online", "last_ip", "start_time", "ping", "load")}, + ), + (_("Capabilities"), {"fields": ("runtimes", "problems")}), + ) + list_display = ("name", "online", "start_time", "ping", "load", "last_ip") + ordering = ["-online", "name"] def get_urls(self): - return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'), - url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] + - super(JudgeAdmin, self).get_urls()) + return [ + url( + r"^(\d+)/disconnect/$", + self.disconnect_view, + name="judge_judge_disconnect", + ), + url( + r"^(\d+)/terminate/$", self.terminate_view, name="judge_judge_terminate" + ), + ] + super(JudgeAdmin, self).get_urls() def disconnect_judge(self, id, force=False): judge = get_object_or_404(Judge, id=id) judge.disconnect(force=force) - return HttpResponseRedirect(reverse('admin:judge_judge_changelist')) + return HttpResponseRedirect(reverse("admin:judge_judge_changelist")) def disconnect_view(self, request, id): return self.disconnect_judge(id) @@ -105,7 +149,7 @@ class JudgeAdmin(VersionAdmin): def get_readonly_fields(self, request, obj=None): if obj is not None and obj.online: - return self.readonly_fields + ('name',) + return self.readonly_fields + ("name",) return self.readonly_fields def has_delete_permission(self, request, obj=None): @@ -116,5 +160,5 @@ class JudgeAdmin(VersionAdmin): if AdminPagedownWidget is not None: formfield_overrides = { - TextField: {'widget': AdminPagedownWidget}, + TextField: {"widget": AdminPagedownWidget}, } diff --git a/judge/admin/submission.py b/judge/admin/submission.py index 2689a76..f621783 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -13,239 +13,367 @@ from django.utils.html import format_html from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext from django_ace import AceWidget -from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \ - SubmissionSource, SubmissionTestCase +from judge.models import ( + ContestParticipation, + ContestProblem, + ContestSubmission, + Profile, + Submission, + SubmissionSource, + SubmissionTestCase, +) from judge.utils.raw_sql import use_straight_join class SubmissionStatusFilter(admin.SimpleListFilter): - parameter_name = title = 'status' - __lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS + parameter_name = title = "status" + __lookups = ( + ("None", _("None")), + ("NotDone", _("Not done")), + ("EX", _("Exceptional")), + ) + Submission.STATUS __handles = set(map(itemgetter(0), Submission.STATUS)) def lookups(self, request, model_admin): return self.__lookups def queryset(self, request, queryset): - if self.value() == 'None': + if self.value() == "None": return queryset.filter(status=None) - elif self.value() == 'NotDone': - return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB']) - elif self.value() == 'EX': - return queryset.exclude(status__in=['D', 'CE', 'G', 'AB']) + elif self.value() == "NotDone": + return queryset.exclude(status__in=["D", "IE", "CE", "AB"]) + elif self.value() == "EX": + return queryset.exclude(status__in=["D", "CE", "G", "AB"]) elif self.value() in self.__handles: return queryset.filter(status=self.value()) class SubmissionResultFilter(admin.SimpleListFilter): - parameter_name = title = 'result' - __lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT + parameter_name = title = "result" + __lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT __handles = set(map(itemgetter(0), Submission.RESULT)) def lookups(self, request, model_admin): return self.__lookups def queryset(self, request, queryset): - if self.value() == 'None': + if self.value() == "None": return queryset.filter(result=None) - elif self.value() == 'BAD': - return queryset.exclude(result='AC') + elif self.value() == "BAD": + return queryset.exclude(result="AC") elif self.value() in self.__handles: return queryset.filter(result=self.value()) class SubmissionTestCaseInline(admin.TabularInline): - fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total') - readonly_fields = ('case', 'batch', 'total') + fields = ("case", "batch", "status", "time", "memory", "points", "total") + readonly_fields = ("case", "batch", "total") model = SubmissionTestCase can_delete = False max_num = 0 class ContestSubmissionInline(admin.StackedInline): - fields = ('problem', 'participation', 'points') + fields = ("problem", "participation", "points") model = ContestSubmission def get_formset(self, request, obj=None, **kwargs): - kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj) + kwargs["formfield_callback"] = partial( + self.formfield_for_dbfield, request=request, obj=obj + ) return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs) def formfield_for_dbfield(self, db_field, **kwargs): - submission = kwargs.pop('obj', None) + submission = kwargs.pop("obj", None) label = None if submission: - if db_field.name == 'participation': - kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user, - contest__problems=submission.problem) \ - .only('id', 'contest__name') + if db_field.name == "participation": + kwargs["queryset"] = ContestParticipation.objects.filter( + user=submission.user, contest__problems=submission.problem + ).only("id", "contest__name") def label(obj): return obj.contest.name - elif db_field.name == 'problem': - kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \ - .only('id', 'problem__name', 'contest__name') + + elif db_field.name == "problem": + kwargs["queryset"] = ContestProblem.objects.filter( + problem=submission.problem + ).only("id", "problem__name", "contest__name") def label(obj): - return pgettext('contest problem', '%(problem)s in %(contest)s') % { - 'problem': obj.problem.name, 'contest': obj.contest.name, + return pgettext("contest problem", "%(problem)s in %(contest)s") % { + "problem": obj.problem.name, + "contest": obj.contest.name, } - field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs) + + field = super(ContestSubmissionInline, self).formfield_for_dbfield( + db_field, **kwargs + ) if label is not None: field.label_from_instance = label return field class SubmissionSourceInline(admin.StackedInline): - fields = ('source',) + fields = ("source",) model = SubmissionSource can_delete = False extra = 0 def get_formset(self, request, obj=None, **kwargs): - kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace, - theme=request.profile.ace_theme) + kwargs.setdefault("widgets", {})["source"] = AceWidget( + mode=obj and obj.language.ace, theme=request.profile.ace_theme + ) return super().get_formset(request, obj, **kwargs) class SubmissionAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'problem', 'date', 'judged_date') - fields = ('user', 'problem', 'date', 'judged_date', 'time', 'memory', 'points', 'language', 'status', 'result', - 'case_points', 'case_total', 'judged_on', 'error') - actions = ('judge', 'recalculate_score') - list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory', - 'points', 'language_column', 'status', 'result', 'judge_column') - list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter) - search_fields = ('problem__code', 'problem__name', 'user__user__username') + readonly_fields = ("user", "problem", "date", "judged_date") + fields = ( + "user", + "problem", + "date", + "judged_date", + "time", + "memory", + "points", + "language", + "status", + "result", + "case_points", + "case_total", + "judged_on", + "error", + ) + actions = ("judge", "recalculate_score") + list_display = ( + "id", + "problem_code", + "problem_name", + "user_column", + "execution_time", + "pretty_memory", + "points", + "language_column", + "status", + "result", + "judge_column", + ) + list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter) + search_fields = ("problem__code", "problem__name", "user__user__username") actions_on_top = True actions_on_bottom = True - inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline] + inlines = [ + SubmissionSourceInline, + SubmissionTestCaseInline, + ContestSubmissionInline, + ] def get_queryset(self, request): - queryset = Submission.objects.select_related('problem', 'user__user', 'language').only( - 'problem__code', 'problem__name', 'user__user__username', 'language__name', - 'time', 'memory', 'points', 'status', 'result', + queryset = Submission.objects.select_related( + "problem", "user__user", "language" + ).only( + "problem__code", + "problem__name", + "user__user__username", + "language__name", + "time", + "memory", + "points", + "status", + "result", ) use_straight_join(queryset) - if not request.user.has_perm('judge.edit_all_problem'): + if not request.user.has_perm("judge.edit_all_problem"): id = request.profile.id - queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct() + queryset = queryset.filter( + Q(problem__authors__id=id) | Q(problem__curators__id=id) + ).distinct() return queryset def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm("judge.edit_own_problem"): return False - if request.user.has_perm('judge.edit_all_problem') or obj is None: + if request.user.has_perm("judge.edit_all_problem") or obj is None: return True return obj.problem.is_editor(request.profile) def lookup_allowed(self, key, value): - return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',) + return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ( + "problem__code", + ) def judge(self, request, queryset): - if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): - self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), - level=messages.ERROR) + if not request.user.has_perm( + "judge.rejudge_submission" + ) or not request.user.has_perm("judge.edit_own_problem"): + self.message_user( + request, + gettext("You do not have the permission to rejudge submissions."), + level=messages.ERROR, + ) return - queryset = queryset.order_by('id') - if not request.user.has_perm('judge.rejudge_submission_lot') and \ - queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT: - self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'), - level=messages.ERROR) + queryset = queryset.order_by("id") + if ( + not request.user.has_perm("judge.rejudge_submission_lot") + and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT + ): + self.message_user( + request, + gettext( + "You do not have the permission to rejudge THAT many submissions." + ), + level=messages.ERROR, + ) return - if not request.user.has_perm('judge.edit_all_problem'): + if not request.user.has_perm("judge.edit_all_problem"): id = request.profile.id - queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)) + queryset = queryset.filter( + Q(problem__authors__id=id) | Q(problem__curators__id=id) + ) judged = len(queryset) for model in queryset: model.judge(rejudge=True, batch_rejudge=True) - self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.', - '%d submissions were successfully scheduled for rejudging.', - judged) % judged) - judge.short_description = _('Rejudge the selected submissions') + self.message_user( + request, + ungettext( + "%d submission was successfully scheduled for rejudging.", + "%d submissions were successfully scheduled for rejudging.", + judged, + ) + % judged, + ) + + judge.short_description = _("Rejudge the selected submissions") def recalculate_score(self, request, queryset): - if not request.user.has_perm('judge.rejudge_submission'): - self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), - level=messages.ERROR) + if not request.user.has_perm("judge.rejudge_submission"): + self.message_user( + request, + gettext("You do not have the permission to rejudge submissions."), + level=messages.ERROR, + ) return - submissions = list(queryset.defer(None).select_related(None).select_related('problem') - .only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points')) + submissions = list( + queryset.defer(None) + .select_related(None) + .select_related("problem") + .only( + "points", + "case_points", + "case_total", + "problem__partial", + "problem__points", + ) + ) for submission in submissions: - submission.points = round(submission.case_points / submission.case_total * submission.problem.points - if submission.case_total else 0, 1) - if not submission.problem.partial and submission.points < submission.problem.points: + submission.points = round( + submission.case_points + / submission.case_total + * submission.problem.points + if submission.case_total + else 0, + 1, + ) + if ( + not submission.problem.partial + and submission.points < submission.problem.points + ): submission.points = 0 submission.save() submission.update_contest() - for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()): + for profile in Profile.objects.filter( + id__in=queryset.values_list("user_id", flat=True).distinct() + ): profile.calculate_points() - cache.delete('user_complete:%d' % profile.id) - cache.delete('user_attempted:%d' % profile.id) + cache.delete("user_complete:%d" % profile.id) + cache.delete("user_attempted:%d" % profile.id) for participation in ContestParticipation.objects.filter( - id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'): + id__in=queryset.values_list("contest__participation_id") + ).prefetch_related("contest"): participation.recompute_results() - self.message_user(request, ungettext('%d submission were successfully rescored.', - '%d submissions were successfully rescored.', - len(submissions)) % len(submissions)) - recalculate_score.short_description = _('Rescore the selected submissions') + self.message_user( + request, + ungettext( + "%d submission were successfully rescored.", + "%d submissions were successfully rescored.", + len(submissions), + ) + % len(submissions), + ) + + recalculate_score.short_description = _("Rescore the selected submissions") def problem_code(self, obj): return obj.problem.code - problem_code.short_description = _('Problem code') - problem_code.admin_order_field = 'problem__code' + + problem_code.short_description = _("Problem code") + problem_code.admin_order_field = "problem__code" def problem_name(self, obj): return obj.problem.name - problem_name.short_description = _('Problem name') - problem_name.admin_order_field = 'problem__name' + + problem_name.short_description = _("Problem name") + problem_name.admin_order_field = "problem__name" def user_column(self, obj): return obj.user.user.username - user_column.admin_order_field = 'user__user__username' - user_column.short_description = _('User') + + user_column.admin_order_field = "user__user__username" + user_column.short_description = _("User") def execution_time(self, obj): - return round(obj.time, 2) if obj.time is not None else 'None' - execution_time.short_description = _('Time') - execution_time.admin_order_field = 'time' + return round(obj.time, 2) if obj.time is not None else "None" + + execution_time.short_description = _("Time") + execution_time.admin_order_field = "time" def pretty_memory(self, obj): memory = obj.memory if memory is None: - return gettext('None') + return gettext("None") if memory < 1000: - return gettext('%d KB') % memory + return gettext("%d KB") % memory else: - return gettext('%.2f MB') % (memory / 1024) - pretty_memory.admin_order_field = 'memory' - pretty_memory.short_description = _('Memory') + return gettext("%.2f MB") % (memory / 1024) + + pretty_memory.admin_order_field = "memory" + pretty_memory.short_description = _("Memory") def language_column(self, obj): return obj.language.name - language_column.admin_order_field = 'language__name' - language_column.short_description = _('Language') + + language_column.admin_order_field = "language__name" + language_column.short_description = _("Language") def judge_column(self, obj): - return format_html('', obj.id) - judge_column.short_description = '' + return format_html( + '', + obj.id, + ) + + judge_column.short_description = "" def get_urls(self): return [ - url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'), + url(r"^(\d+)/judge/$", self.judge_view, name="judge_submission_rejudge"), ] + super(SubmissionAdmin, self).get_urls() def judge_view(self, request, id): - if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm( + "judge.rejudge_submission" + ) or not request.user.has_perm("judge.edit_own_problem"): raise PermissionDenied() submission = get_object_or_404(Submission, id=id) - if not request.user.has_perm('judge.edit_all_problem') and \ - not submission.problem.is_editor(request.profile): + if not request.user.has_perm( + "judge.edit_all_problem" + ) and not submission.problem.is_editor(request.profile): raise PermissionDenied() submission.judge(rejudge=True) - return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) diff --git a/judge/admin/taxon.py b/judge/admin/taxon.py index c707144..4335efe 100644 --- a/judge/admin/taxon.py +++ b/judge/admin/taxon.py @@ -8,45 +8,51 @@ from judge.widgets import AdminHeavySelect2MultipleWidget class ProblemGroupForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Included problems'), + label=_("Included problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are included in this group of problems'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are included in this group of problems"), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class ProblemGroupAdmin(admin.ModelAdmin): - fields = ('name', 'full_name', 'problems') + fields = ("name", "full_name", "problems") form = ProblemGroupForm def save_model(self, request, obj, form, change): super(ProblemGroupAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(form.cleaned_data['problems']) + obj.problem_set.set(form.cleaned_data["problems"]) obj.save() def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] + self.form.base_fields["problems"].initial = ( + [o.pk for o in obj.problem_set.all()] if obj else [] + ) return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs) class ProblemTypeForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Included problems'), + label=_("Included problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are included in this type of problems'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are included in this type of problems"), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class ProblemTypeAdmin(admin.ModelAdmin): - fields = ('name', 'full_name', 'problems') + fields = ("name", "full_name", "problems") form = ProblemTypeForm def save_model(self, request, obj, form, change): super(ProblemTypeAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(form.cleaned_data['problems']) + obj.problem_set.set(form.cleaned_data["problems"]) obj.save() def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] + self.form.base_fields["problems"].initial = ( + [o.pk for o in obj.problem_set.all()] if obj else [] + ) return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs) diff --git a/judge/admin/ticket.py b/judge/admin/ticket.py index a5c5a5e..f7495dd 100644 --- a/judge/admin/ticket.py +++ b/judge/admin/ticket.py @@ -4,36 +4,56 @@ from django.forms import ModelForm from django.urls import reverse_lazy from judge.models import TicketMessage -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + HeavyPreviewAdminPageDownWidget, +) class TicketMessageForm(ModelForm): class Meta: widgets = { - 'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + "user": AdminHeavySelect2Widget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } if HeavyPreviewAdminPageDownWidget is not None: - widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview')) + widgets["body"] = HeavyPreviewAdminPageDownWidget( + preview=reverse_lazy("ticket_preview") + ) class TicketMessageInline(StackedInline): model = TicketMessage form = TicketMessageForm - fields = ('user', 'body') + fields = ("user", "body") class TicketForm(ModelForm): class Meta: widgets = { - 'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + "user": AdminHeavySelect2Widget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), + "assignees": AdminHeavySelect2MultipleWidget( + data_view="profile_select2", attrs={"style": "width: 100%"} + ), } class TicketAdmin(ModelAdmin): - fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes') - readonly_fields = ('time',) - list_display = ('title', 'user', 'time', 'linked_item') + fields = ( + "title", + "time", + "user", + "assignees", + "content_type", + "object_id", + "notes", + ) + readonly_fields = ("time",) + list_display = ("title", "user", "time", "linked_item") inlines = [TicketMessageInline] form = TicketForm - date_hierarchy = 'time' + date_hierarchy = "time" diff --git a/judge/admin/volunteer.py b/judge/admin/volunteer.py index 58ebc86..a26cfd6 100644 --- a/judge/admin/volunteer.py +++ b/judge/admin/volunteer.py @@ -5,14 +5,30 @@ from django.utils.translation import gettext, gettext_lazy as _, ungettext from judge.models import VolunteerProblemVote + class VolunteerProblemVoteAdmin(admin.ModelAdmin): - fields = ('voter', 'problem', 'time', 'thinking_points', 'knowledge_points', 'feedback') - readonly_fields = ('time', 'problem', 'voter') - list_display = ('voter', 'problem_link', 'time', 'thinking_points', 'knowledge_points', 'feedback') - date_hierarchy = 'time' + fields = ( + "voter", + "problem", + "time", + "thinking_points", + "knowledge_points", + "feedback", + ) + readonly_fields = ("time", "problem", "voter") + list_display = ( + "voter", + "problem_link", + "time", + "thinking_points", + "knowledge_points", + "feedback", + ) + date_hierarchy = "time" def problem_link(self, obj): - url = reverse('admin:judge_problem_change', args=(obj.problem.id,)) + url = reverse("admin:judge_problem_change", args=(obj.problem.id,)) return format_html(f"{obj.problem.code}") - problem_link.short_description = _('Problem') - problem_link.admin_order_field = 'problem__code' \ No newline at end of file + + problem_link.short_description = _("Problem") + problem_link.admin_order_field = "problem__code" diff --git a/judge/apps.py b/judge/apps.py index 96ec022..f223d52 100644 --- a/judge/apps.py +++ b/judge/apps.py @@ -4,8 +4,8 @@ from django.utils.translation import gettext_lazy class JudgeAppConfig(AppConfig): - name = 'judge' - verbose_name = gettext_lazy('Online Judge') + name = "judge" + verbose_name = gettext_lazy("Online Judge") def ready(self): # WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE, diff --git a/judge/bridge/base_handler.py b/judge/bridge/base_handler.py index c3fbd40..369a8e1 100644 --- a/judge/bridge/base_handler.py +++ b/judge/bridge/base_handler.py @@ -8,9 +8,9 @@ from netaddr import IPGlob, IPSet from judge.utils.unicode import utf8text -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") -size_pack = struct.Struct('!I') +size_pack = struct.Struct("!I") assert size_pack.size == 4 MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024 @@ -20,7 +20,7 @@ def proxy_list(human_readable): globs = [] addrs = [] for item in human_readable: - if '*' in item or '-' in item: + if "*" in item or "-" in item: globs.append(IPGlob(item)) else: addrs.append(item) @@ -43,7 +43,7 @@ class RequestHandlerMeta(type): try: handler.handle() except BaseException: - logger.exception('Error in base packet handling') + logger.exception("Error in base packet handling") raise finally: handler.on_disconnect() @@ -70,8 +70,12 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): def read_sized_packet(self, size, initial=None): if size > MAX_ALLOWED_PACKET_SIZE: - logger.log(logging.WARNING if self._got_packet else logging.INFO, - 'Disconnecting client due to too-large message size (%d bytes): %s', size, self.client_address) + logger.log( + logging.WARNING if self._got_packet else logging.INFO, + "Disconnecting client due to too-large message size (%d bytes): %s", + size, + self.client_address, + ) raise Disconnect() buffer = [] @@ -86,7 +90,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): data = self.request.recv(remainder) remainder -= len(data) buffer.append(data) - self._on_packet(b''.join(buffer)) + self._on_packet(b"".join(buffer)) def parse_proxy_protocol(self, line): words = line.split() @@ -94,18 +98,18 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): if len(words) < 2: raise Disconnect() - if words[1] == b'TCP4': + if words[1] == b"TCP4": if len(words) != 6: raise Disconnect() self.client_address = (utf8text(words[2]), utf8text(words[4])) self.server_address = (utf8text(words[3]), utf8text(words[5])) - elif words[1] == b'TCP6': + elif words[1] == b"TCP6": self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0) self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0) - elif words[1] != b'UNKNOWN': + elif words[1] != b"UNKNOWN": raise Disconnect() - def read_size(self, buffer=b''): + def read_size(self, buffer=b""): while len(buffer) < size_pack.size: recv = self.request.recv(size_pack.size - len(buffer)) if not recv: @@ -113,9 +117,9 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): buffer += recv return size_pack.unpack(buffer)[0] - def read_proxy_header(self, buffer=b''): + def read_proxy_header(self, buffer=b""): # Max line length for PROXY protocol is 107, and we received 4 already. - while b'\r\n' not in buffer: + while b"\r\n" not in buffer: if len(buffer) > 107: raise Disconnect() data = self.request.recv(107) @@ -125,7 +129,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): return buffer def _on_packet(self, data): - decompressed = zlib.decompress(data).decode('utf-8') + decompressed = zlib.decompress(data).decode("utf-8") self._got_packet = True self.on_packet(decompressed) @@ -145,8 +149,10 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): try: tag = self.read_size() self._initial_tag = size_pack.pack(tag) - if self.client_address[0] in self.proxies and self._initial_tag == b'PROX': - proxy, _, remainder = self.read_proxy_header(self._initial_tag).partition(b'\r\n') + if self.client_address[0] in self.proxies and self._initial_tag == b"PROX": + proxy, _, remainder = self.read_proxy_header( + self._initial_tag + ).partition(b"\r\n") self.parse_proxy_protocol(proxy) while remainder: @@ -154,8 +160,8 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): self.read_sized_packet(self.read_size(remainder)) break - size = size_pack.unpack(remainder[:size_pack.size])[0] - remainder = remainder[size_pack.size:] + size = size_pack.unpack(remainder[: size_pack.size])[0] + remainder = remainder[size_pack.size :] if len(remainder) <= size: self.read_sized_packet(size, remainder) break @@ -171,25 +177,36 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta): return except zlib.error: if self._got_packet: - logger.warning('Encountered zlib error during packet handling, disconnecting client: %s', - self.client_address, exc_info=True) + logger.warning( + "Encountered zlib error during packet handling, disconnecting client: %s", + self.client_address, + exc_info=True, + ) else: - logger.info('Potentially wrong protocol (zlib error): %s: %r', self.client_address, self._initial_tag, - exc_info=True) + logger.info( + "Potentially wrong protocol (zlib error): %s: %r", + self.client_address, + self._initial_tag, + exc_info=True, + ) except socket.timeout: if self._got_packet: - logger.info('Socket timed out: %s', self.client_address) + logger.info("Socket timed out: %s", self.client_address) self.on_timeout() else: - logger.info('Potentially wrong protocol: %s: %r', self.client_address, self._initial_tag) + logger.info( + "Potentially wrong protocol: %s: %r", + self.client_address, + self._initial_tag, + ) except socket.error as e: # When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex. - if e.__class__.__name__ == 'cancel_wait_ex': + if e.__class__.__name__ == "cancel_wait_ex": return raise def send(self, data): - compressed = zlib.compress(data.encode('utf-8')) + compressed = zlib.compress(data.encode("utf-8")) self.request.sendall(size_pack.pack(len(compressed)) + compressed) def close(self): diff --git a/judge/bridge/daemon.py b/judge/bridge/daemon.py index ce8a702..b9988ce 100644 --- a/judge/bridge/daemon.py +++ b/judge/bridge/daemon.py @@ -11,7 +11,7 @@ from judge.bridge.judge_list import JudgeList from judge.bridge.server import Server from judge.models import Judge, Submission -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") def reset_judges(): @@ -20,12 +20,17 @@ def reset_judges(): def judge_daemon(): reset_judges() - Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \ - .update(status='IE', result='IE', error=None) + Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS).update( + status="IE", result="IE", error=None + ) judges = JudgeList() - judge_server = Server(settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)) - django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)) + judge_server = Server( + settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges) + ) + django_server = Server( + settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges) + ) threading.Thread(target=django_server.serve_forever).start() threading.Thread(target=judge_server.serve_forever).start() @@ -33,7 +38,7 @@ def judge_daemon(): stop = threading.Event() def signal_handler(signum, _): - logger.info('Exiting due to %s', signal.Signals(signum).name) + logger.info("Exiting due to %s", signal.Signals(signum).name) stop.set() signal.signal(signal.SIGINT, signal_handler) diff --git a/judge/bridge/django_handler.py b/judge/bridge/django_handler.py index b284b68..f72aba1 100644 --- a/judge/bridge/django_handler.py +++ b/judge/bridge/django_handler.py @@ -4,8 +4,8 @@ import struct from judge.bridge.base_handler import Disconnect, ZlibPacketHandler -logger = logging.getLogger('judge.bridge') -size_pack = struct.Struct('!I') +logger = logging.getLogger("judge.bridge") +size_pack = struct.Struct("!I") class DjangoHandler(ZlibPacketHandler): @@ -13,47 +13,52 @@ class DjangoHandler(ZlibPacketHandler): super().__init__(request, client_address, server) self.handlers = { - 'submission-request': self.on_submission, - 'terminate-submission': self.on_termination, - 'disconnect-judge': self.on_disconnect_request, + "submission-request": self.on_submission, + "terminate-submission": self.on_termination, + "disconnect-judge": self.on_disconnect_request, } self.judges = judges def send(self, data): - super().send(json.dumps(data, separators=(',', ':'))) + super().send(json.dumps(data, separators=(",", ":"))) def on_packet(self, packet): packet = json.loads(packet) try: - result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet) + result = self.handlers.get(packet.get("name", None), self.on_malformed)( + packet + ) except Exception: - logger.exception('Error in packet handling (Django-facing)') - result = {'name': 'bad-request'} + logger.exception("Error in packet handling (Django-facing)") + result = {"name": "bad-request"} self.send(result) raise Disconnect() def on_submission(self, data): - id = data['submission-id'] - problem = data['problem-id'] - language = data['language'] - source = data['source'] - judge_id = data['judge-id'] - priority = data['priority'] + id = data["submission-id"] + problem = data["problem-id"] + language = data["language"] + source = data["source"] + judge_id = data["judge-id"] + priority = data["priority"] if not self.judges.check_priority(priority): - return {'name': 'bad-request'} + return {"name": "bad-request"} self.judges.judge(id, problem, language, source, judge_id, priority) - return {'name': 'submission-received', 'submission-id': id} + return {"name": "submission-received", "submission-id": id} def on_termination(self, data): - return {'name': 'submission-received', 'judge-aborted': self.judges.abort(data['submission-id'])} + return { + "name": "submission-received", + "judge-aborted": self.judges.abort(data["submission-id"]), + } def on_disconnect_request(self, data): - judge_id = data['judge-id'] - force = data['force'] + judge_id = data["judge-id"] + force = data["force"] self.judges.disconnect(judge_id, force=force) def on_malformed(self, packet): - logger.error('Malformed packet: %s', packet) + logger.error("Malformed packet: %s", packet) def on_close(self): self._to_kill = False diff --git a/judge/bridge/echo_test_client.py b/judge/bridge/echo_test_client.py index 8fec692..801d34e 100644 --- a/judge/bridge/echo_test_client.py +++ b/judge/bridge/echo_test_client.py @@ -4,7 +4,7 @@ import struct import time import zlib -size_pack = struct.Struct('!I') +size_pack = struct.Struct("!I") def open_connection(): @@ -13,69 +13,70 @@ def open_connection(): def zlibify(data): - data = zlib.compress(data.encode('utf-8')) + data = zlib.compress(data.encode("utf-8")) return size_pack.pack(len(data)) + data def dezlibify(data, skip_head=True): if skip_head: - data = data[size_pack.size:] - return zlib.decompress(data).decode('utf-8') + data = data[size_pack.size :] + return zlib.decompress(data).decode("utf-8") def main(): global host, port import argparse + parser = argparse.ArgumentParser() - parser.add_argument('-l', '--host', default='localhost') - parser.add_argument('-p', '--port', default=9999, type=int) + parser.add_argument("-l", "--host", default="localhost") + parser.add_argument("-p", "--port", default=9999, type=int) args = parser.parse_args() host, port = args.host, args.port - print('Opening idle connection:', end=' ') + print("Opening idle connection:", end=" ") s1 = open_connection() - print('Success') - print('Opening hello world connection:', end=' ') + print("Success") + print("Opening hello world connection:", end=" ") s2 = open_connection() - print('Success') - print('Sending Hello, World!', end=' ') - s2.sendall(zlibify('Hello, World!')) - print('Success') - print('Testing blank connection:', end=' ') + print("Success") + print("Sending Hello, World!", end=" ") + s2.sendall(zlibify("Hello, World!")) + print("Success") + print("Testing blank connection:", end=" ") s3 = open_connection() s3.close() - print('Success') + print("Success") result = dezlibify(s2.recv(1024)) - assert result == 'Hello, World!' + assert result == "Hello, World!" print(result) s2.close() - print('Large random data test:', end=' ') + print("Large random data test:", end=" ") s4 = open_connection() - data = os.urandom(1000000).decode('iso-8859-1') - print('Generated', end=' ') + data = os.urandom(1000000).decode("iso-8859-1") + print("Generated", end=" ") s4.sendall(zlibify(data)) - print('Sent', end=' ') - result = b'' + print("Sent", end=" ") + result = b"" while len(result) < size_pack.size: result += s4.recv(1024) - size = size_pack.unpack(result[:size_pack.size])[0] - result = result[size_pack.size:] + size = size_pack.unpack(result[: size_pack.size])[0] + result = result[size_pack.size :] while len(result) < size: result += s4.recv(1024) - print('Received', end=' ') + print("Received", end=" ") assert dezlibify(result, False) == data - print('Success') + print("Success") s4.close() - print('Test malformed connection:', end=' ') + print("Test malformed connection:", end=" ") s5 = open_connection() - s5.sendall(data[:100000].encode('utf-8')) + s5.sendall(data[:100000].encode("utf-8")) s5.close() - print('Success') - print('Waiting for timeout to close idle connection:', end=' ') + print("Success") + print("Waiting for timeout to close idle connection:", end=" ") time.sleep(6) - print('Done') + print("Done") s1.close() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/judge/bridge/echo_test_server.py b/judge/bridge/echo_test_server.py index 59e21fa..2d84e85 100644 --- a/judge/bridge/echo_test_server.py +++ b/judge/bridge/echo_test_server.py @@ -3,19 +3,22 @@ from judge.bridge.base_handler import ZlibPacketHandler class EchoPacketHandler(ZlibPacketHandler): def on_connect(self): - print('New client:', self.client_address) + print("New client:", self.client_address) self.timeout = 5 def on_timeout(self): - print('Inactive client:', self.client_address) + print("Inactive client:", self.client_address) def on_packet(self, data): self.timeout = None - print('Data from %s: %r' % (self.client_address, data[:30] if len(data) > 30 else data)) + print( + "Data from %s: %r" + % (self.client_address, data[:30] if len(data) > 30 else data) + ) self.send(data) def on_disconnect(self): - print('Closed client:', self.client_address) + print("Closed client:", self.client_address) def main(): @@ -23,9 +26,9 @@ def main(): from judge.bridge.server import Server parser = argparse.ArgumentParser() - parser.add_argument('-l', '--host', action='append') - parser.add_argument('-p', '--port', type=int, action='append') - parser.add_argument('-P', '--proxy', action='append') + parser.add_argument("-l", "--host", action="append") + parser.add_argument("-p", "--port", type=int, action="append") + parser.add_argument("-P", "--proxy", action="append") args = parser.parse_args() class Handler(EchoPacketHandler): @@ -35,5 +38,5 @@ def main(): server.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/judge/bridge/judge_handler.py b/judge/bridge/judge_handler.py index 86f53b4..a6c3b10 100644 --- a/judge/bridge/judge_handler.py +++ b/judge/bridge/judge_handler.py @@ -14,19 +14,30 @@ from django.db.models import F from judge import event_poster as event from judge.bridge.base_handler import ZlibPacketHandler, proxy_list from judge.caching import finished_submission -from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase +from judge.models import ( + Judge, + Language, + LanguageLimit, + Problem, + RuntimeVersion, + Submission, + SubmissionTestCase, +) -logger = logging.getLogger('judge.bridge') -json_log = logging.getLogger('judge.json.bridge') +logger = logging.getLogger("judge.bridge") +json_log = logging.getLogger("judge.json.bridge") UPDATE_RATE_LIMIT = 5 UPDATE_RATE_TIME = 0.5 -SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id') +SubmissionData = namedtuple( + "SubmissionData", + "time memory short_circuit pretests_only contest_no attempt_no user_id", +) def _ensure_connection(): try: - db.connection.cursor().execute('SELECT 1').fetchall() + db.connection.cursor().execute("SELECT 1").fetchall() except Exception: db.connection.close() @@ -39,19 +50,19 @@ class JudgeHandler(ZlibPacketHandler): self.judges = judges self.handlers = { - 'grading-begin': self.on_grading_begin, - 'grading-end': self.on_grading_end, - 'compile-error': self.on_compile_error, - 'compile-message': self.on_compile_message, - 'batch-begin': self.on_batch_begin, - 'batch-end': self.on_batch_end, - 'test-case-status': self.on_test_case, - 'internal-error': self.on_internal_error, - 'submission-terminated': self.on_submission_terminated, - 'submission-acknowledged': self.on_submission_acknowledged, - 'ping-response': self.on_ping_response, - 'supported-problems': self.on_supported_problems, - 'handshake': self.on_handshake, + "grading-begin": self.on_grading_begin, + "grading-end": self.on_grading_end, + "compile-error": self.on_compile_error, + "compile-message": self.on_compile_message, + "batch-begin": self.on_batch_begin, + "batch-end": self.on_batch_end, + "test-case-status": self.on_test_case, + "internal-error": self.on_internal_error, + "submission-terminated": self.on_submission_terminated, + "submission-acknowledged": self.on_submission_acknowledged, + "ping-response": self.on_ping_response, + "supported-problems": self.on_supported_problems, + "handshake": self.on_handshake, } self._working = False self._no_response_job = None @@ -78,22 +89,38 @@ class JudgeHandler(ZlibPacketHandler): def on_connect(self): self.timeout = 15 - logger.info('Judge connected from: %s', self.client_address) - json_log.info(self._make_json_log(action='connect')) + logger.info("Judge connected from: %s", self.client_address) + json_log.info(self._make_json_log(action="connect")) def on_disconnect(self): self._stop_ping.set() if self._working: - logger.error('Judge %s disconnected while handling submission %s', self.name, self._working) + logger.error( + "Judge %s disconnected while handling submission %s", + self.name, + self._working, + ) self.judges.remove(self) if self.name is not None: self._disconnected() - logger.info('Judge disconnected from: %s with name %s', self.client_address, self.name) + logger.info( + "Judge disconnected from: %s with name %s", self.client_address, self.name + ) - json_log.info(self._make_json_log(action='disconnect', info='judge disconnected')) + json_log.info( + self._make_json_log(action="disconnect", info="judge disconnected") + ) if self._working: - Submission.objects.filter(id=self._working).update(status='IE', result='IE', error='') - json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading')) + Submission.objects.filter(id=self._working).update( + status="IE", result="IE", error="" + ) + json_log.error( + self._make_json_log( + sub=self._working, + action="close", + info="IE due to shutdown on grading", + ) + ) def _authenticate(self, id, key): try: @@ -104,7 +131,11 @@ class JudgeHandler(ZlibPacketHandler): result = hmac.compare_digest(judge.auth_key, key) if not result: - json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication')) + json_log.warning( + self._make_json_log( + action="auth", judge=id, info="judge failed authentication" + ) + ) return result def _connected(self): @@ -119,15 +150,29 @@ class JudgeHandler(ZlibPacketHandler): versions = [] for lang in judge.runtimes.all(): versions += [ - RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge) + RuntimeVersion( + language=lang, + name=name, + version=".".join(map(str, version)), + priority=idx, + judge=judge, + ) for idx, (name, version) in enumerate(self.executors[lang.key]) ] RuntimeVersion.objects.bulk_create(versions) judge.last_ip = self.client_address[0] judge.save() - self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1]) - json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated', - executors=list(self.executors.keys()))) + self.judge_address = "[%s]:%s" % ( + self.client_address[0], + self.client_address[1], + ) + json_log.info( + self._make_json_log( + action="auth", + info="judge successfully authenticated", + executors=list(self.executors.keys()), + ) + ) def _disconnected(self): Judge.objects.filter(id=self.judge.id).update(online=False) @@ -135,40 +180,50 @@ class JudgeHandler(ZlibPacketHandler): def _update_ping(self): try: - Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load) + Judge.objects.filter(name=self.name).update( + ping=self.latency, load=self.load + ) except Exception as e: # What can I do? I don't want to tie this to MySQL. - if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006: + if ( + e.__class__.__name__ == "OperationalError" + and e.__module__ == "_mysql_exceptions" + and e.args[0] == 2006 + ): db.connection.close() def send(self, data): - super().send(json.dumps(data, separators=(',', ':'))) + super().send(json.dumps(data, separators=(",", ":"))) def on_handshake(self, packet): - if 'id' not in packet or 'key' not in packet: - logger.warning('Malformed handshake: %s', self.client_address) + if "id" not in packet or "key" not in packet: + logger.warning("Malformed handshake: %s", self.client_address) self.close() return - if not self._authenticate(packet['id'], packet['key']): - logger.warning('Authentication failure: %s', self.client_address) + if not self._authenticate(packet["id"], packet["key"]): + logger.warning("Authentication failure: %s", self.client_address) self.close() return self.timeout = 60 - self._problems = packet['problems'] + self._problems = packet["problems"] self.problems = dict(self._problems) - self.executors = packet['executors'] - self.name = packet['id'] + self.executors = packet["executors"] + self.name = packet["id"] - self.send({'name': 'handshake-success'}) - logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id']) + self.send({"name": "handshake-success"}) + logger.info("Judge authenticated: %s (%s)", self.client_address, packet["id"]) self.judges.register(self) threading.Thread(target=self._ping_thread).start() self._connected() def can_judge(self, problem, executor, judge_id=None): - return problem in self.problems and executor in self.executors and (not judge_id or self.name == judge_id) + return ( + problem in self.problems + and executor in self.executors + and (not judge_id or self.name == judge_id) + ) @property def working(self): @@ -178,25 +233,60 @@ class JudgeHandler(ZlibPacketHandler): _ensure_connection() try: - pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = ( - Submission.objects.filter(id=submission) - .values_list('problem__id', 'problem__time_limit', 'problem__memory_limit', - 'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id', - 'contest__participation__virtual', 'contest__participation__id')).get() + ( + pid, + time, + memory, + short_circuit, + lid, + is_pretested, + sub_date, + uid, + part_virtual, + part_id, + ) = ( + Submission.objects.filter(id=submission).values_list( + "problem__id", + "problem__time_limit", + "problem__memory_limit", + "problem__short_circuit", + "language__id", + "is_pretested", + "date", + "user__id", + "contest__participation__virtual", + "contest__participation__id", + ) + ).get() except Submission.DoesNotExist: - logger.error('Submission vanished: %s', submission) - json_log.error(self._make_json_log( - sub=self._working, action='request', - info='submission vanished when fetching info', - )) + logger.error("Submission vanished: %s", submission) + json_log.error( + self._make_json_log( + sub=self._working, + action="request", + info="submission vanished when fetching info", + ) + ) return - attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid, - date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1 + attempt_no = ( + Submission.objects.filter( + problem__id=pid, + contest__participation__id=part_id, + user__id=uid, + date__lt=sub_date, + ) + .exclude(status__in=("CE", "IE")) + .count() + + 1 + ) try: - time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid) - .values_list('time_limit', 'memory_limit').get()) + time, memory = ( + LanguageLimit.objects.filter(problem__id=pid, language__id=lid) + .values_list("time_limit", "memory_limit") + .get() + ) except LanguageLimit.DoesNotExist: pass @@ -215,139 +305,184 @@ class JudgeHandler(ZlibPacketHandler): # Yank the power out. self.close() else: - self.send({'name': 'disconnect'}) + self.send({"name": "disconnect"}) def submit(self, id, problem, language, source): data = self.get_related_submission_data(id) self._working = id self._no_response_job = threading.Timer(20, self._kill_if_no_response) - self.send({ - 'name': 'submission-request', - 'submission-id': id, - 'problem-id': problem, - 'language': language, - 'source': source, - 'time-limit': data.time, - 'memory-limit': data.memory, - 'short-circuit': data.short_circuit, - 'meta': { - 'pretests-only': data.pretests_only, - 'in-contest': data.contest_no, - 'attempt-no': data.attempt_no, - 'user': data.user_id, - }, - }) + self.send( + { + "name": "submission-request", + "submission-id": id, + "problem-id": problem, + "language": language, + "source": source, + "time-limit": data.time, + "memory-limit": data.memory, + "short-circuit": data.short_circuit, + "meta": { + "pretests-only": data.pretests_only, + "in-contest": data.contest_no, + "attempt-no": data.attempt_no, + "user": data.user_id, + }, + } + ) def _kill_if_no_response(self): - logger.error('Judge failed to acknowledge submission: %s: %s', self.name, self._working) + logger.error( + "Judge failed to acknowledge submission: %s: %s", self.name, self._working + ) self.close() def on_timeout(self): if self.name: - logger.warning('Judge seems dead: %s: %s', self.name, self._working) + logger.warning("Judge seems dead: %s: %s", self.name, self._working) def malformed_packet(self, exception): - logger.exception('Judge sent malformed packet: %s', self.name) + logger.exception("Judge sent malformed packet: %s", self.name) super(JudgeHandler, self).malformed_packet(exception) def on_submission_processing(self, packet): _ensure_connection() - id = packet['submission-id'] - if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge): - event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'}) - self._post_update_submission(id, 'processing') - json_log.info(self._make_json_log(packet, action='processing')) + id = packet["submission-id"] + if Submission.objects.filter(id=id).update(status="P", judged_on=self.judge): + event.post("sub_%s" % Submission.get_id_secret(id), {"type": "processing"}) + self._post_update_submission(id, "processing") + json_log.info(self._make_json_log(packet, action="processing")) else: - logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='processing', info='unknown submission')) + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, action="processing", info="unknown submission" + ) + ) def on_submission_wrong_acknowledge(self, packet, expected, got): - json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected)) - Submission.objects.filter(id=expected).update(status='IE', result='IE', error=None) - Submission.objects.filter(id=got, status='QU').update(status='IE', result='IE', error=None) + json_log.error( + self._make_json_log( + packet, action="processing", info="wrong-acknowledge", expected=expected + ) + ) + Submission.objects.filter(id=expected).update( + status="IE", result="IE", error=None + ) + Submission.objects.filter(id=got, status="QU").update( + status="IE", result="IE", error=None + ) def on_submission_acknowledged(self, packet): - if not packet.get('submission-id', None) == self._working: - logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None), - self._working) - self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None)) + if not packet.get("submission-id", None) == self._working: + logger.error( + "Wrong acknowledgement: %s: %s, expected: %s", + self.name, + packet.get("submission-id", None), + self._working, + ) + self.on_submission_wrong_acknowledge( + packet, self._working, packet.get("submission-id", None) + ) self.close() - logger.info('Submission acknowledged: %d', self._working) + logger.info("Submission acknowledged: %d", self._working) if self._no_response_job: self._no_response_job.cancel() self._no_response_job = None self.on_submission_processing(packet) def abort(self): - self.send({'name': 'terminate-submission'}) + self.send({"name": "terminate-submission"}) def get_current_submission(self): return self._working or None def ping(self): - self.send({'name': 'ping', 'when': time.time()}) + self.send({"name": "ping", "when": time.time()}) def on_packet(self, data): try: try: data = json.loads(data) - if 'name' not in data: + if "name" not in data: raise ValueError except ValueError: self.on_malformed(data) else: - handler = self.handlers.get(data['name'], self.on_malformed) + handler = self.handlers.get(data["name"], self.on_malformed) handler(data) except Exception: - logger.exception('Error in packet handling (Judge-side): %s', self.name) + logger.exception("Error in packet handling (Judge-side): %s", self.name) self._packet_exception() # You can't crash here because you aren't so sure about the judges # not being malicious or simply malforms. THIS IS A SERVER! def _packet_exception(self): - json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception')) + json_log.exception( + self._make_json_log(sub=self._working, info="packet processing exception") + ) def _submission_is_batch(self, id): if not Submission.objects.filter(id=id).update(batch=True): - logger.warning('Unknown submission: %s', id) + logger.warning("Unknown submission: %s", id) def on_supported_problems(self, packet): - logger.info('%s: Updated problem list', self.name) - self._problems = packet['problems'] + logger.info("%s: Updated problem list", self.name) + self._problems = packet["problems"] self.problems = dict(self._problems) if not self.working: self.judges.update_problems(self) - self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys()))) - json_log.info(self._make_json_log(action='update-problems', count=len(self.problems))) + self.judge.problems.set( + Problem.objects.filter(code__in=list(self.problems.keys())) + ) + json_log.info( + self._make_json_log(action="update-problems", count=len(self.problems)) + ) def on_grading_begin(self, packet): - logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id']) + logger.info("%s: Grading has begun on: %s", self.name, packet["submission-id"]) self.batch_id = None - if Submission.objects.filter(id=packet['submission-id']).update( - status='G', is_pretested=packet['pretested'], - current_testcase=1, points=0, - batch=False, judged_date=timezone.now()): - SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete() - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'}) - self._post_update_submission(packet['submission-id'], 'grading-begin') - json_log.info(self._make_json_log(packet, action='grading-begin')) + if Submission.objects.filter(id=packet["submission-id"]).update( + status="G", + is_pretested=packet["pretested"], + current_testcase=1, + points=0, + batch=False, + judged_date=timezone.now(), + ): + SubmissionTestCase.objects.filter( + submission_id=packet["submission-id"] + ).delete() + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + {"type": "grading-begin"}, + ) + self._post_update_submission(packet["submission-id"], "grading-begin") + json_log.info(self._make_json_log(packet, action="grading-begin")) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, action="grading-begin", info="unknown submission" + ) + ) def on_grading_end(self, packet): - logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id']) + logger.info("%s: Grading has ended on: %s", self.name, packet["submission-id"]) self._free_self(packet) self.batch_id = None try: - submission = Submission.objects.get(id=packet['submission-id']) + submission = Submission.objects.get(id=packet["submission-id"]) except Submission.DoesNotExist: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, action="grading-end", info="unknown submission" + ) + ) return time = 0 @@ -355,7 +490,7 @@ class JudgeHandler(ZlibPacketHandler): points = 0.0 total = 0 status = 0 - status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE'] + status_codes = ["SC", "AC", "WA", "MLE", "TLE", "IR", "RTE", "OLE"] batches = {} # batch number: (points, total) for case in SubmissionTestCase.objects.filter(submission=submission): @@ -388,19 +523,29 @@ class JudgeHandler(ZlibPacketHandler): if not problem.partial and sub_points != problem.points: sub_points = 0 - submission.status = 'D' + submission.status = "D" submission.time = time submission.memory = memory submission.points = sub_points submission.result = status_codes[status] submission.save() - json_log.info(self._make_json_log( - packet, action='grading-end', time=time, memory=memory, - points=sub_points, total=problem.points, result=submission.result, - case_points=points, case_total=total, user=submission.user_id, - problem=problem.code, finish=True, - )) + json_log.info( + self._make_json_log( + packet, + action="grading-end", + time=time, + memory=memory, + points=sub_points, + total=problem.points, + result=submission.result, + case_points=points, + case_total=total, + user=submission.user_id, + problem=problem.code, + finish=True, + ) + ) submission.user._updating_stats_only = True submission.user.calculate_points() @@ -410,143 +555,254 @@ class JudgeHandler(ZlibPacketHandler): finished_submission(submission) - event.post('sub_%s' % submission.id_secret, { - 'type': 'grading-end', - 'time': time, - 'memory': memory, - 'points': float(points), - 'total': float(problem.points), - 'result': submission.result, - }) - if hasattr(submission, 'contest'): + event.post( + "sub_%s" % submission.id_secret, + { + "type": "grading-end", + "time": time, + "memory": memory, + "points": float(points), + "total": float(problem.points), + "result": submission.result, + }, + ) + if hasattr(submission, "contest"): participation = submission.contest.participation - event.post('contest_%d' % participation.contest_id, {'type': 'update'}) - self._post_update_submission(submission.id, 'grading-end', done=True) + event.post("contest_%d" % participation.contest_id, {"type": "update"}) + self._post_update_submission(submission.id, "grading-end", done=True) def on_compile_error(self, packet): - logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id']) + logger.info( + "%s: Submission failed to compile: %s", self.name, packet["submission-id"] + ) self._free_self(packet) - if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), { - 'type': 'compile-error', - 'log': packet['log'], - }) - self._post_update_submission(packet['submission-id'], 'compile-error', done=True) - json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'], - finish=True, result='CE')) + if Submission.objects.filter(id=packet["submission-id"]).update( + status="CE", result="CE", error=packet["log"] + ): + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + { + "type": "compile-error", + "log": packet["log"], + }, + ) + self._post_update_submission( + packet["submission-id"], "compile-error", done=True + ) + json_log.info( + self._make_json_log( + packet, + action="compile-error", + log=packet["log"], + finish=True, + result="CE", + ) + ) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission', - log=packet['log'], finish=True, result='CE')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, + action="compile-error", + info="unknown submission", + log=packet["log"], + finish=True, + result="CE", + ) + ) def on_compile_message(self, packet): - logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id']) + logger.info( + "%s: Submission generated compiler messages: %s", + self.name, + packet["submission-id"], + ) - if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'}) - json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log'])) + if Submission.objects.filter(id=packet["submission-id"]).update( + error=packet["log"] + ): + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + {"type": "compile-message"}, + ) + json_log.info( + self._make_json_log(packet, action="compile-message", log=packet["log"]) + ) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission', - log=packet['log'])) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, + action="compile-message", + info="unknown submission", + log=packet["log"], + ) + ) def on_internal_error(self, packet): try: - raise ValueError('\n\n' + packet['message']) + raise ValueError("\n\n" + packet["message"]) except ValueError: - logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id']) + logger.exception( + "Judge %s failed while handling submission %s", + self.name, + packet["submission-id"], + ) self._free_self(packet) - id = packet['submission-id'] - if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']): - event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'}) - self._post_update_submission(id, 'internal-error', done=True) - json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'], - finish=True, result='IE')) + id = packet["submission-id"] + if Submission.objects.filter(id=id).update( + status="IE", result="IE", error=packet["message"] + ): + event.post( + "sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"} + ) + self._post_update_submission(id, "internal-error", done=True) + json_log.info( + self._make_json_log( + packet, + action="internal-error", + message=packet["message"], + finish=True, + result="IE", + ) + ) else: - logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission', - message=packet['message'], finish=True, result='IE')) + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, + action="internal-error", + info="unknown submission", + message=packet["message"], + finish=True, + result="IE", + ) + ) def on_submission_terminated(self, packet): - logger.info('%s: Submission aborted: %s', self.name, packet['submission-id']) + logger.info("%s: Submission aborted: %s", self.name, packet["submission-id"]) self._free_self(packet) - if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'}) - self._post_update_submission(packet['submission-id'], 'terminated', done=True) - json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB')) + if Submission.objects.filter(id=packet["submission-id"]).update( + status="AB", result="AB" + ): + event.post( + "sub_%s" % Submission.get_id_secret(packet["submission-id"]), + {"type": "aborted-submission"}, + ) + self._post_update_submission( + packet["submission-id"], "terminated", done=True + ) + json_log.info( + self._make_json_log(packet, action="aborted", finish=True, result="AB") + ) else: - logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission', - finish=True, result='AB')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, + action="aborted", + info="unknown submission", + finish=True, + result="AB", + ) + ) def on_batch_begin(self, packet): - logger.info('%s: Batch began on: %s', self.name, packet['submission-id']) + logger.info("%s: Batch began on: %s", self.name, packet["submission-id"]) self.in_batch = True if self.batch_id is None: self.batch_id = 0 - self._submission_is_batch(packet['submission-id']) + self._submission_is_batch(packet["submission-id"]) self.batch_id += 1 - json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id)) + json_log.info( + self._make_json_log(packet, action="batch-begin", batch=self.batch_id) + ) def on_batch_end(self, packet): self.in_batch = False - logger.info('%s: Batch ended on: %s', self.name, packet['submission-id']) - json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id)) + logger.info("%s: Batch ended on: %s", self.name, packet["submission-id"]) + json_log.info( + self._make_json_log(packet, action="batch-end", batch=self.batch_id) + ) - def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length): - logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id']) + def on_test_case( + self, + packet, + max_feedback=SubmissionTestCase._meta.get_field("feedback").max_length, + ): + logger.info( + "%s: %d test case(s) completed on: %s", + self.name, + len(packet["cases"]), + packet["submission-id"], + ) - id = packet['submission-id'] - updates = packet['cases'] - max_position = max(map(itemgetter('position'), updates)) - sum_points = sum(map(itemgetter('points'), updates)) + id = packet["submission-id"] + updates = packet["cases"] + max_position = max(map(itemgetter("position"), updates)) + sum_points = sum(map(itemgetter("points"), updates)) - - if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1, points=F('points') + sum_points): - logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission')) + if not Submission.objects.filter(id=id).update( + current_testcase=max_position + 1, points=F("points") + sum_points + ): + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, action="test-case", info="unknown submission" + ) + ) return bulk_test_case_updates = [] for result in updates: - test_case = SubmissionTestCase(submission_id=id, case=result['position']) - status = result['status'] + test_case = SubmissionTestCase(submission_id=id, case=result["position"]) + status = result["status"] if status & 4: - test_case.status = 'TLE' + test_case.status = "TLE" elif status & 8: - test_case.status = 'MLE' + test_case.status = "MLE" elif status & 64: - test_case.status = 'OLE' + test_case.status = "OLE" elif status & 2: - test_case.status = 'RTE' + test_case.status = "RTE" elif status & 16: - test_case.status = 'IR' + test_case.status = "IR" elif status & 1: - test_case.status = 'WA' + test_case.status = "WA" elif status & 32: - test_case.status = 'SC' + test_case.status = "SC" else: - test_case.status = 'AC' - test_case.time = result['time'] - test_case.memory = result['memory'] - test_case.points = result['points'] - test_case.total = result['total-points'] + test_case.status = "AC" + test_case.time = result["time"] + test_case.memory = result["memory"] + test_case.points = result["points"] + test_case.total = result["total-points"] test_case.batch = self.batch_id if self.in_batch else None - test_case.feedback = (result.get('feedback') or '')[:max_feedback] - test_case.extended_feedback = result.get('extended-feedback') or '' - test_case.output = result['output'] + test_case.feedback = (result.get("feedback") or "")[:max_feedback] + test_case.extended_feedback = result.get("extended-feedback") or "" + test_case.output = result["output"] bulk_test_case_updates.append(test_case) - json_log.info(self._make_json_log( - packet, action='test-case', case=test_case.case, batch=test_case.batch, - time=test_case.time, memory=test_case.memory, feedback=test_case.feedback, - extended_feedback=test_case.extended_feedback, output=test_case.output, - points=test_case.points, total=test_case.total, status=test_case.status, - )) + json_log.info( + self._make_json_log( + packet, + action="test-case", + case=test_case.case, + batch=test_case.batch, + time=test_case.time, + memory=test_case.memory, + feedback=test_case.feedback, + extended_feedback=test_case.extended_feedback, + output=test_case.output, + points=test_case.points, + total=test_case.total, + status=test_case.status, + ) + ) do_post = True @@ -563,29 +819,34 @@ class JudgeHandler(ZlibPacketHandler): self.update_counter[id] = (1, time.monotonic()) if do_post: - event.post('sub_%s' % Submission.get_id_secret(id), { - 'type': 'test-case', - 'id': max_position, - }) - self._post_update_submission(id, state='test-case') + event.post( + "sub_%s" % Submission.get_id_secret(id), + { + "type": "test-case", + "id": max_position, + }, + ) + self._post_update_submission(id, state="test-case") SubmissionTestCase.objects.bulk_create(bulk_test_case_updates) def on_malformed(self, packet): - logger.error('%s: Malformed packet: %s', self.name, packet) - json_log.exception(self._make_json_log(sub=self._working, info='malformed json packet')) + logger.error("%s: Malformed packet: %s", self.name, packet) + json_log.exception( + self._make_json_log(sub=self._working, info="malformed json packet") + ) def on_ping_response(self, packet): end = time.time() - self._ping_average.append(end - packet['when']) - self._time_delta.append((end + packet['when']) / 2 - packet['time']) + self._ping_average.append(end - packet["when"]) + self._time_delta.append((end + packet["when"]) / 2 - packet["time"]) self.latency = sum(self._ping_average) / len(self._ping_average) self.time_delta = sum(self._time_delta) / len(self._time_delta) - self.load = packet['load'] + self.load = packet["load"] self._update_ping() def _free_self(self, packet): - self.judges.on_judge_free(self, packet['submission-id']) + self.judges.on_judge_free(self, packet["submission-id"]) def _ping_thread(self): try: @@ -594,19 +855,19 @@ class JudgeHandler(ZlibPacketHandler): if self._stop_ping.wait(10): break except Exception: - logger.exception('Ping error in %s', self.name) + logger.exception("Ping error in %s", self.name) self.close() raise def _make_json_log(self, packet=None, sub=None, **kwargs): data = { - 'judge': self.name, - 'address': self.judge_address, + "judge": self.name, + "address": self.judge_address, } if sub is None and packet is not None: - sub = packet.get('submission-id') + sub = packet.get("submission-id") if sub is not None: - data['submission'] = sub + data["submission"] = sub data.update(kwargs) return json.dumps(data) @@ -614,17 +875,31 @@ class JudgeHandler(ZlibPacketHandler): if self._submission_cache_id == id: data = self._submission_cache else: - self._submission_cache = data = Submission.objects.filter(id=id).values( - 'problem__is_public', 'contest_object__key', - 'user_id', 'problem_id', 'status', 'language__key', - ).get() + self._submission_cache = data = ( + Submission.objects.filter(id=id) + .values( + "problem__is_public", + "contest_object__key", + "user_id", + "problem_id", + "status", + "language__key", + ) + .get() + ) self._submission_cache_id = id - if data['problem__is_public']: - event.post('submissions', { - 'type': 'done-submission' if done else 'update-submission', - 'state': state, 'id': id, - 'contest': data['contest_object__key'], - 'user': data['user_id'], 'problem': data['problem_id'], - 'status': data['status'], 'language': data['language__key'], - }) + if data["problem__is_public"]: + event.post( + "submissions", + { + "type": "done-submission" if done else "update-submission", + "state": state, + "id": id, + "contest": data["contest_object__key"], + "user": data["user_id"], + "problem": data["problem_id"], + "status": data["status"], + "language": data["language__key"], + }, + ) diff --git a/judge/bridge/judge_list.py b/judge/bridge/judge_list.py index 828bb83..a670667 100644 --- a/judge/bridge/judge_list.py +++ b/judge/bridge/judge_list.py @@ -8,9 +8,9 @@ try: except ImportError: from pyllist import dllist -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") -PriorityMarker = namedtuple('PriorityMarker', 'priority') +PriorityMarker = namedtuple("PriorityMarker", "priority") class JudgeList(object): @@ -18,7 +18,9 @@ class JudgeList(object): def __init__(self): self.queue = dllist() - self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)] + self.priority = [ + self.queue.append(PriorityMarker(i)) for i in range(self.priorities) + ] self.judges = set() self.node_map = {} self.submission_map = {} @@ -32,11 +34,19 @@ class JudgeList(object): id, problem, language, source, judge_id = node.value if judge.can_judge(problem, language, judge_id): self.submission_map[id] = judge - logger.info('Dispatched queued submission %d: %s', id, judge.name) + logger.info( + "Dispatched queued submission %d: %s", id, judge.name + ) try: judge.submit(id, problem, language, source) except Exception: - logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) + logger.exception( + "Failed to dispatch %d (%s, %s) to %s", + id, + problem, + language, + judge.name, + ) self.judges.remove(judge) return self.queue.remove(node) @@ -76,14 +86,14 @@ class JudgeList(object): def on_judge_free(self, judge, submission): with self.lock: - logger.info('Judge available after grading %d: %s', submission, judge.name) + logger.info("Judge available after grading %d: %s", submission, judge.name) del self.submission_map[submission] judge._working = False self._handle_free_judge(judge) def abort(self, submission): with self.lock: - logger.info('Abort request: %d', submission) + logger.info("Abort request: %d", submission) try: self.submission_map[submission].abort() return True @@ -108,21 +118,33 @@ class JudgeList(object): return candidates = [ - judge for judge in self.judges if not judge.working and judge.can_judge(problem, language, judge_id) + judge + for judge in self.judges + if not judge.working and judge.can_judge(problem, language, judge_id) ] if judge_id: - logger.info('Specified judge %s is%savailable', judge_id, ' ' if candidates else ' not ') + logger.info( + "Specified judge %s is%savailable", + judge_id, + " " if candidates else " not ", + ) else: - logger.info('Free judges: %d', len(candidates)) + logger.info("Free judges: %d", len(candidates)) if candidates: # Schedule the submission on the judge reporting least load. - judge = min(candidates, key=attrgetter('load')) - logger.info('Dispatched submission %d to: %s', id, judge.name) + judge = min(candidates, key=attrgetter("load")) + logger.info("Dispatched submission %d to: %s", id, judge.name) self.submission_map[id] = judge try: judge.submit(id, problem, language, source) except Exception: - logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) + logger.exception( + "Failed to dispatch %d (%s, %s) to %s", + id, + problem, + language, + judge.name, + ) self.judges.discard(judge) return self.judge(id, problem, language, source, judge_id, priority) else: @@ -130,4 +152,4 @@ class JudgeList(object): (id, problem, language, source, judge_id), self.priority[priority], ) - logger.info('Queued submission: %d', id) + logger.info("Queued submission: %d", id) diff --git a/judge/bridge/server.py b/judge/bridge/server.py index cc83f84..4e67310 100644 --- a/judge/bridge/server.py +++ b/judge/bridge/server.py @@ -12,7 +12,9 @@ class Server: self._shutdown = threading.Event() def serve_forever(self): - threads = [threading.Thread(target=server.serve_forever) for server in self.servers] + threads = [ + threading.Thread(target=server.serve_forever) for server in self.servers + ] for thread in threads: thread.daemon = True thread.start() diff --git a/judge/caching.py b/judge/caching.py index 7f0a687..99bbf81 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -2,9 +2,9 @@ from django.core.cache import cache def finished_submission(sub): - keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id] - if hasattr(sub, 'contest'): + keys = ["user_complete:%d" % sub.user_id, "user_attempted:%s" % sub.user_id] + if hasattr(sub, "contest"): participation = sub.contest.participation - keys += ['contest_complete:%d' % participation.id] - keys += ['contest_attempted:%d' % participation.id] + keys += ["contest_complete:%d" % participation.id] + keys += ["contest_attempted:%d" % participation.id] cache.delete_many(keys) diff --git a/judge/comments.py b/judge/comments.py index 7408e68..2b7579a 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -7,7 +7,11 @@ from django.db.models import Count from django.db.models.expressions import F, Value from django.db.models.functions import Coalesce from django.forms import ModelForm -from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect +from django.http import ( + HttpResponseForbidden, + HttpResponseNotFound, + HttpResponseRedirect, +) from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ @@ -24,50 +28,55 @@ from judge.widgets import HeavyPreviewPageDownWidget from judge.jinja2.reference import get_user_from_text - def add_mention_notifications(comment): user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id) for user in user_referred: - notification_ref = Notification(owner=user, - comment=comment, - category='Mention') + notification_ref = Notification(owner=user, comment=comment, category="Mention") notification_ref.save() -def del_mention_notifications(comment): - query = { - 'comment': comment, - 'category': 'Mention' - } - Notification.objects.filter(**query).delete() +def del_mention_notifications(comment): + query = {"comment": comment, "category": "Mention"} + Notification.objects.filter(**query).delete() class CommentForm(ModelForm): class Meta: model = Comment - fields = ['body', 'parent'] + fields = ["body", "parent"] widgets = { - 'parent': forms.HiddenInput(), + "parent": forms.HiddenInput(), } if HeavyPreviewPageDownWidget is not None: - widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'), - preview_timeout=1000, hide_preview_button=True) + widgets["body"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("comment_preview"), + preview_timeout=1000, + hide_preview_button=True, + ) def __init__(self, request, *args, **kwargs): self.request = request super(CommentForm, self).__init__(*args, **kwargs) - self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')}) + self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")}) def clean(self): if self.request is not None and self.request.user.is_authenticated: profile = self.request.profile if profile.mute: - raise ValidationError(_('Your part is silent, little toad.')) - elif (not self.request.user.is_staff and - not profile.submission_set.filter(points=F('problem__points')).exists()): - raise ValidationError(_('You need to have solved at least one problem ' - 'before your voice can be heard.')) + raise ValidationError(_("Your part is silent, little toad.")) + elif ( + not self.request.user.is_staff + and not profile.submission_set.filter( + points=F("problem__points") + ).exists() + ): + raise ValidationError( + _( + "You need to have solved at least one problem " + "before your voice can be heard." + ) + ) return super(CommentForm, self).clean() @@ -80,10 +89,12 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): return self.comment_page def is_comment_locked(self): - if 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)) + 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): @@ -93,14 +104,16 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): if self.is_comment_locked(): return HttpResponseForbidden() - parent = request.POST.get('parent') + parent = request.POST.get("parent") if parent: try: parent = int(parent) except ValueError: return HttpResponseNotFound() else: - if not Comment.objects.filter(hidden=False, id=parent, page=page).exists(): + if not Comment.objects.filter( + hidden=False, id=parent, page=page + ).exists(): return HttpResponseNotFound() form = CommentForm(request, request.POST) @@ -109,21 +122,22 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): comment.author = request.profile comment.page = page - - with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision(): + with LockModel( + write=(Comment, Revision, Version), read=(ContentType,) + ), revisions.create_revision(): revisions.set_user(request.user) - revisions.set_comment(_('Posted comment')) + revisions.set_comment(_("Posted comment")) comment.save() # add notification for reply if comment.parent and comment.parent.author != comment.author: - notification_rep = Notification(owner=comment.parent.author, - comment=comment, - category='Reply') + notification_rep = Notification( + owner=comment.parent.author, comment=comment, category="Reply" + ) notification_rep.save() add_mention_notifications(comment) - + return HttpResponseRedirect(request.path) context = self.get_context_data(object=self.object, comment_form=form) @@ -131,25 +145,40 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): self.object = self.get_object() - return self.render_to_response(self.get_context_data( - object=self.object, - comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}), - )) + return self.render_to_response( + self.get_context_data( + object=self.object, + comment_form=CommentForm( + request, initial={"page": self.get_comment_page(), "parent": None} + ), + ) + ) def get_context_data(self, **kwargs): context = super(CommentedDetailView, self).get_context_data(**kwargs) queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page()) - context['has_comments'] = queryset.exists() - context['comment_lock'] = self.is_comment_locked() - queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions')) + context["has_comments"] = queryset.exists() + context["comment_lock"] = self.is_comment_locked() + queryset = ( + queryset.select_related("author__user") + .defer("author__about") + .annotate(revisions=Count("versions")) + ) if self.request.user.is_authenticated: - queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0))) + queryset = queryset.annotate( + vote_score=Coalesce(RawSQLColumn(CommentVote, "score"), Value(0)) + ) profile = self.request.profile - unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id) - context['is_new_user'] = (not self.request.user.is_staff and - not profile.submission_set.filter(points=F('problem__points')).exists()) - context['comment_list'] = queryset - context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD + unique_together_left_join( + queryset, CommentVote, "comment", "voter", profile.id + ) + context["is_new_user"] = ( + not self.request.user.is_staff + and not profile.submission_set.filter( + points=F("problem__points") + ).exists() + ) + context["comment_list"] = queryset + context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD return context - diff --git a/judge/contest_format/atcoder.py b/judge/contest_format/atcoder.py index 28b188d..bb3b30a 100644 --- a/judge/contest_format/atcoder.py +++ b/judge/contest_format/atcoder.py @@ -14,14 +14,14 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('atcoder') +@register_contest_format("atcoder") class AtCoderContestFormat(DefaultContestFormat): - name = gettext_lazy('AtCoder') - config_defaults = {'penalty': 5} - config_validators = {'penalty': lambda x: x >= 0} - ''' + name = gettext_lazy("AtCoder") + config_defaults = {"penalty": 5} + config_validators = {"penalty": lambda x: x >= 0} + """ penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5. - ''' + """ @classmethod def validate(cls, config): @@ -29,7 +29,9 @@ class AtCoderContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('AtCoder-styled contest expects no config or dict as config') + raise ValidationError( + "AtCoder-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -37,7 +39,9 @@ class AtCoderContestFormat(DefaultContestFormat): 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)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -51,7 +55,8 @@ class AtCoderContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT MAX(cs.points) as `score`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -62,21 +67,27 @@ class AtCoderContestFormat(DefaultContestFormat): 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)) + """, + (participation.id, participation.id), + ) for score, time, prob in cursor.fetchall(): time = from_database_time(time) dt = (time - participation.start).total_seconds() # Compute penalty - if self.config['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) + subs = ( + participation.submissions.exclude( + submission__result__isnull=True + ) + .exclude(submission__result__in=["IE", "CE"]) + .filter(problem_id=prob) + ) if score: prev = subs.filter(submission__date__lte=time).count() - 1 - penalty += prev * self.config['penalty'] * 60 + 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() @@ -86,7 +97,7 @@ class AtCoderContestFormat(DefaultContestFormat): if score: cumtime = max(cumtime, dt) - format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev} + format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev} points += score participation.cumtime = cumtime + penalty @@ -98,17 +109,38 @@ class AtCoderContestFormat(DefaultContestFormat): 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(' ({penalty})', - penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' + penalty = ( + format_html( + ' ({penalty})', + penalty=floatformat(format_data["penalty"]), + ) + if format_data["penalty"] + else "" + ) return format_html( '{points}{penalty}
{time}
', - 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']), + 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'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: return mark_safe('') diff --git a/judge/contest_format/base.py b/judge/contest_format/base.py index d4e6e6f..9e573d7 100644 --- a/judge/contest_format/base.py +++ b/judge/contest_format/base.py @@ -93,7 +93,7 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)): @classmethod def best_solution_state(cls, points, total): if not points: - return 'failed-score' + return "failed-score" if points == total: - return 'full-score' - return 'partial-score' + return "full-score" + return "partial-score" diff --git a/judge/contest_format/default.py b/judge/contest_format/default.py index 532102d..11c8f37 100644 --- a/judge/contest_format/default.py +++ b/judge/contest_format/default.py @@ -13,14 +13,16 @@ from judge.contest_format.registry import register_contest_format from judge.utils.timedelta import nice_repr -@register_contest_format('default') +@register_contest_format("default") class DefaultContestFormat(BaseContestFormat): - name = gettext_lazy('Default') + name = gettext_lazy("Default") @classmethod def validate(cls, config): if config is not None and (not isinstance(config, dict) or config): - raise ValidationError('default contest expects no config or empty dict as config') + raise ValidationError( + "default contest expects no config or empty dict as config" + ) def __init__(self, contest, config): super(DefaultContestFormat, self).__init__(contest, config) @@ -30,14 +32,18 @@ class DefaultContestFormat(BaseContestFormat): points = 0 format_data = {} - for result in participation.submissions.values('problem_id').annotate( - time=Max('submission__date'), points=Max('points'), + for result in participation.submissions.values("problem_id").annotate( + time=Max("submission__date"), + points=Max("points"), ): - dt = (result['time'] - participation.start).total_seconds() - if result['points']: + dt = (result["time"] - participation.start).total_seconds() + if result["points"]: cumtime += dt - format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']} - points += result['points'] + format_data[str(result["problem_id"])] = { + "time": dt, + "points": result["points"], + } + points += result["points"] participation.cumtime = max(cumtime, 0) participation.score = points @@ -49,30 +55,50 @@ class DefaultContestFormat(BaseContestFormat): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: return format_html( - u'{points}
{time}
', - 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'], -self.contest.points_precision), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + '{points}
{time}
', + 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"], -self.contest.points_precision + ), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: return mark_safe('') def display_participation_result(self, participation): return format_html( - u'{points}
{cumtime}
', + '{points}
{cumtime}
', points=floatformat(participation.score, -self.contest.points_precision), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'), + cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday"), ) def get_problem_breakdown(self, participation, contest_problems): - return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems] + return [ + (participation.format_data or {}).get(str(contest_problem.id)) + for contest_problem in contest_problems + ] def get_contest_problem_label_script(self): - return ''' + return """ function(n) return tostring(math.floor(n + 1)) end - ''' \ No newline at end of file + """ diff --git a/judge/contest_format/ecoo.py b/judge/contest_format/ecoo.py index 1199826..012be13 100644 --- a/judge/contest_format/ecoo.py +++ b/judge/contest_format/ecoo.py @@ -14,17 +14,21 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('ecoo') +@register_contest_format("ecoo") class ECOOContestFormat(DefaultContestFormat): - name = gettext_lazy('ECOO') - config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5} - config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0} - ''' + name = gettext_lazy("ECOO") + config_defaults = {"cumtime": False, "first_ac_bonus": 10, "time_bonus": 5} + config_validators = { + "cumtime": lambda x: True, + "first_ac_bonus": lambda x: x >= 0, + "time_bonus": lambda x: x >= 0, + } + """ cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False. first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10. time_bonus: Number of minutes to award an extra point for submitting before the contest end. Specify 0 to disable. Defaults to 5. - ''' + """ @classmethod def validate(cls, config): @@ -32,7 +36,9 @@ class ECOOContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('ECOO-styled contest expects no config or dict as config') + raise ValidationError( + "ECOO-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -40,7 +46,9 @@ class ECOOContestFormat(DefaultContestFormat): 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)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -53,7 +61,8 @@ class ECOOContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT ( SELECT MAX(ccs.points) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -69,25 +78,31 @@ class ECOOContestFormat(DefaultContestFormat): 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, participation.id)) + """, + (participation.id, participation.id, participation.id), + ) for score, time, prob, subs, max_score in cursor.fetchall(): time = from_database_time(time) dt = (time - participation.start).total_seconds() - if self.config['cumtime']: + if self.config["cumtime"]: cumtime += dt bonus = 0 if score > 0: # First AC bonus if subs == 1 and score == max_score: - bonus += self.config['first_ac_bonus'] + bonus += self.config["first_ac_bonus"] # Time bonus - if self.config['time_bonus']: - bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus'] + if self.config["time_bonus"]: + bonus += ( + (participation.end_time - time).total_seconds() + // 60 + // self.config["time_bonus"] + ) points += bonus - format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus} + format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus} points += score participation.cumtime = cumtime @@ -99,25 +114,47 @@ class ECOOContestFormat(DefaultContestFormat): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - bonus = format_html(' +{bonus}', - bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else '' + bonus = ( + format_html( + " +{bonus}", bonus=floatformat(format_data["bonus"]) + ) + if format_data["bonus"] + else "" + ) return format_html( '{points}{bonus}
{time}
', - 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']), + 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"]), bonus=bonus, - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") def display_participation_result(self, participation): return format_html( '{points}
{cumtime}
', points=floatformat(participation.score), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', + cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday") + if self.config["cumtime"] + else "", ) diff --git a/judge/contest_format/icpc.py b/judge/contest_format/icpc.py index e858a6c..8596628 100644 --- a/judge/contest_format/icpc.py +++ b/judge/contest_format/icpc.py @@ -14,14 +14,14 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('icpc') +@register_contest_format("icpc") class ICPCContestFormat(DefaultContestFormat): - name = gettext_lazy('ICPC') - config_defaults = {'penalty': 20} - config_validators = {'penalty': lambda x: x >= 0} - ''' + 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): @@ -29,7 +29,9 @@ class ICPCContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('ICPC-styled contest expects no config or dict as config') + 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: @@ -37,7 +39,9 @@ class ICPCContestFormat(DefaultContestFormat): 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)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -52,7 +56,8 @@ class ICPCContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT MAX(cs.points) as `points`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -63,21 +68,27 @@ class ICPCContestFormat(DefaultContestFormat): 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)) + """, + (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']: + 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) + 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 + 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() @@ -88,7 +99,7 @@ class ICPCContestFormat(DefaultContestFormat): cumtime += dt last = max(last, dt) - format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev} + format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev} score += points participation.cumtime = max(0, cumtime + penalty) @@ -100,23 +111,44 @@ class ICPCContestFormat(DefaultContestFormat): 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(' ({penalty})', - penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' + penalty = ( + format_html( + ' ({penalty})', + penalty=floatformat(format_data["penalty"]), + ) + if format_data["penalty"] + else "" + ) return format_html( '{points}{penalty}
{time}
', - 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']), + 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'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") def get_contest_problem_label_script(self): - return ''' + return """ function(n) n = n + 1 ret = "" @@ -126,4 +158,4 @@ class ICPCContestFormat(DefaultContestFormat): end return ret end - ''' \ No newline at end of file + """ diff --git a/judge/contest_format/ioi.py b/judge/contest_format/ioi.py index dba337c..2113fab 100644 --- a/judge/contest_format/ioi.py +++ b/judge/contest_format/ioi.py @@ -14,13 +14,13 @@ from judge.timezone import from_database_time from judge.utils.timedelta import nice_repr -@register_contest_format('ioi') +@register_contest_format("ioi") class IOIContestFormat(DefaultContestFormat): - name = gettext_lazy('IOI') - config_defaults = {'cumtime': False} - ''' + name = gettext_lazy("IOI") + config_defaults = {"cumtime": False} + """ cumtime: Specify True if time penalties are to be computed. Defaults to False. - ''' + """ @classmethod def validate(cls, config): @@ -28,7 +28,9 @@ class IOIContestFormat(DefaultContestFormat): return if not isinstance(config, dict): - raise ValidationError('IOI-styled contest expects no config or dict as config') + raise ValidationError( + "IOI-styled contest expects no config or dict as config" + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -47,7 +49,8 @@ class IOIContestFormat(DefaultContestFormat): format_data = {} with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT MAX(cs.points) as `score`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -58,17 +61,21 @@ class IOIContestFormat(DefaultContestFormat): 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)) + """, + (participation.id, participation.id), + ) for score, time, prob in cursor.fetchall(): - if self.config['cumtime']: - dt = (from_database_time(time) - participation.start).total_seconds() + if self.config["cumtime"]: + dt = ( + from_database_time(time) - participation.start + ).total_seconds() if score: cumtime += dt else: dt = 0 - format_data[str(prob)] = {'time': dt, 'points': score} + format_data[str(prob)] = {"time": dt, "points": score} points += score participation.cumtime = max(cumtime, 0) @@ -82,12 +89,29 @@ class IOIContestFormat(DefaultContestFormat): if format_data: return format_html( '{points}
{time}
', - 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']), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '', + 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"]), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday") + if self.config["cumtime"] + else "", ) else: return mark_safe('') @@ -96,5 +120,7 @@ class IOIContestFormat(DefaultContestFormat): return format_html( '{points}
{cumtime}
', points=floatformat(participation.score), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', + cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday") + if self.config["cumtime"] + else "", ) diff --git a/judge/dblock.py b/judge/dblock.py index d4d5184..5b7feab 100644 --- a/judge/dblock.py +++ b/judge/dblock.py @@ -5,19 +5,21 @@ from django.db import connection, transaction class LockModel(object): def __init__(self, write, read=()): - self.tables = ', '.join(chain( - ('`%s` WRITE' % model._meta.db_table for model in write), - ('`%s` READ' % model._meta.db_table for model in read), - )) + self.tables = ", ".join( + chain( + ("`%s` WRITE" % model._meta.db_table for model in write), + ("`%s` READ" % model._meta.db_table for model in read), + ) + ) self.cursor = connection.cursor() def __enter__(self): - self.cursor.execute('LOCK TABLES ' + self.tables) + self.cursor.execute("LOCK TABLES " + self.tables) def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: transaction.commit() else: transaction.rollback() - self.cursor.execute('UNLOCK TABLES') + self.cursor.execute("UNLOCK TABLES") self.cursor.close() diff --git a/judge/event_poster.py b/judge/event_poster.py index 29100bd..e7c57fd 100644 --- a/judge/event_poster.py +++ b/judge/event_poster.py @@ -1,6 +1,6 @@ from django.conf import settings -__all__ = ['last', 'post'] +__all__ = ["last", "post"] if not settings.EVENT_DAEMON_USE: real = False @@ -10,9 +10,12 @@ if not settings.EVENT_DAEMON_USE: def last(): return 0 -elif hasattr(settings, 'EVENT_DAEMON_AMQP'): + +elif hasattr(settings, "EVENT_DAEMON_AMQP"): from .event_poster_amqp import last, post + real = True else: from .event_poster_ws import last, post + real = True diff --git a/judge/event_poster_amqp.py b/judge/event_poster_amqp.py index 74f6331..959d882 100644 --- a/judge/event_poster_amqp.py +++ b/judge/event_poster_amqp.py @@ -6,7 +6,7 @@ import pika from django.conf import settings from pika.exceptions import AMQPError -__all__ = ['EventPoster', 'post', 'last'] +__all__ = ["EventPoster", "post", "last"] class EventPoster(object): @@ -15,14 +15,19 @@ class EventPoster(object): self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE def _connect(self): - self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP)) + self._conn = pika.BlockingConnection( + pika.URLParameters(settings.EVENT_DAEMON_AMQP) + ) self._chan = self._conn.channel() def post(self, channel, message, tries=0): try: id = int(time() * 1000000) - self._chan.basic_publish(self._exchange, '', - json.dumps({'id': id, 'channel': channel, 'message': message})) + self._chan.basic_publish( + self._exchange, + "", + json.dumps({"id": id, "channel": channel, "message": message}), + ) return id except AMQPError: if tries > 10: @@ -35,7 +40,7 @@ _local = threading.local() def _get_poster(): - if 'poster' not in _local.__dict__: + if "poster" not in _local.__dict__: _local.poster = EventPoster() return _local.poster diff --git a/judge/event_poster_ws.py b/judge/event_poster_ws.py index fba4052..8daddab 100644 --- a/judge/event_poster_ws.py +++ b/judge/event_poster_ws.py @@ -5,7 +5,7 @@ import threading from django.conf import settings from websocket import WebSocketException, create_connection -__all__ = ['EventPostingError', 'EventPoster', 'post', 'last'] +__all__ = ["EventPostingError", "EventPoster", "post", "last"] _local = threading.local() @@ -20,19 +20,23 @@ class EventPoster(object): def _connect(self): self._conn = create_connection(settings.EVENT_DAEMON_POST) if settings.EVENT_DAEMON_KEY is not None: - self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY})) + self._conn.send( + json.dumps({"command": "auth", "key": settings.EVENT_DAEMON_KEY}) + ) resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) def post(self, channel, message, tries=0): try: - self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message})) + self._conn.send( + json.dumps({"command": "post", "channel": channel, "message": message}) + ) resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) else: - return resp['id'] + return resp["id"] except WebSocketException: if tries > 10: raise @@ -43,10 +47,10 @@ class EventPoster(object): try: self._conn.send('{"command": "last-msg"}') resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) else: - return resp['id'] + return resp["id"] except WebSocketException: if tries > 10: raise @@ -55,7 +59,7 @@ class EventPoster(object): def _get_poster(): - if 'poster' not in _local.__dict__: + if "poster" not in _local.__dict__: _local.poster = EventPoster() return _local.poster diff --git a/judge/feed.py b/judge/feed.py index 6d8d825..d665dd6 100644 --- a/judge/feed.py +++ b/judge/feed.py @@ -12,27 +12,35 @@ import re # https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/ -def escape_xml_illegal_chars(val, replacement='?'): - _illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]') +def escape_xml_illegal_chars(val, replacement="?"): + _illegal_xml_chars_RE = re.compile( + "[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]" + ) return _illegal_xml_chars_RE.sub(replacement, val) + class ProblemFeed(Feed): - title = 'Recently Added %s Problems' % settings.SITE_NAME - link = '/' - description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME + title = "Recently Added %s Problems" % settings.SITE_NAME + link = "/" + description = ( + "The latest problems added on the %s website" % settings.SITE_LONG_NAME + ) def items(self): - return Problem.objects.filter(is_public=True, is_organization_private=False).defer('description')\ - .order_by('-date', '-id')[:25] + return ( + Problem.objects.filter(is_public=True, is_organization_private=False) + .defer("description") + .order_by("-date", "-id")[:25] + ) def item_title(self, problem): return problem.name def item_description(self, problem): - key = 'problem_feed:%d' % problem.id + key = "problem_feed:%d" % problem.id desc = cache.get(key) if desc is None: - desc = str(markdown(problem.description, 'problem'))[:500] + '...' + desc = str(markdown(problem.description, "problem"))[:500] + "..." desc = escape_xml_illegal_chars(desc) cache.set(key, desc, 86400) return desc @@ -49,21 +57,21 @@ class AtomProblemFeed(ProblemFeed): class CommentFeed(Feed): - title = 'Latest %s Comments' % settings.SITE_NAME - link = '/' - description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME + title = "Latest %s Comments" % settings.SITE_NAME + link = "/" + description = "The latest comments on the %s website" % settings.SITE_LONG_NAME def items(self): return Comment.most_recent(AnonymousUser(), 25) def item_title(self, comment): - return '%s -> %s' % (comment.author.user.username, comment.page_title) + return "%s -> %s" % (comment.author.user.username, comment.page_title) def item_description(self, comment): - key = 'comment_feed:%d' % comment.id + key = "comment_feed:%d" % comment.id desc = cache.get(key) if desc is None: - desc = str(markdown(comment.body, 'comment')) + desc = str(markdown(comment.body, "comment")) desc = escape_xml_illegal_chars(desc) cache.set(key, desc, 86400) return desc @@ -80,21 +88,23 @@ class AtomCommentFeed(CommentFeed): class BlogFeed(Feed): - title = 'Latest %s Blog Posts' % settings.SITE_NAME - link = '/' - description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME + title = "Latest %s Blog Posts" % settings.SITE_NAME + link = "/" + description = "The latest blog posts from the %s" % settings.SITE_LONG_NAME def items(self): - return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on') + return BlogPost.objects.filter( + visible=True, publish_on__lte=timezone.now() + ).order_by("-sticky", "-publish_on") def item_title(self, post): return post.title def item_description(self, post): - key = 'blog_feed:%d' % post.id + key = "blog_feed:%d" % post.id summary = cache.get(key) if summary is None: - summary = str(markdown(post.summary or post.content, 'blog')) + summary = str(markdown(post.summary or post.content, "blog")) summary = escape_xml_illegal_chars(summary) cache.set(key, summary, 86400) return summary diff --git a/judge/forms.py b/judge/forms.py index d704fc5..a6166bc 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -12,159 +12,211 @@ from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django_ace import AceWidget -from judge.models import Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, Submission +from judge.models import ( + Contest, + Language, + Organization, + PrivateMessage, + Problem, + ProblemPointsVote, + Profile, + Submission, +) from judge.utils.subscription import newsletter_id -from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \ - Select2Widget +from judge.widgets import ( + HeavyPreviewPageDownWidget, + MathJaxPagedownWidget, + PagedownWidget, + Select2MultipleWidget, + Select2Widget, +) -def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')): - return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c' +def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")): + return ( + string + (sum(k in unsafe for k in string) - string.count("\u202c")) * "\u202c" + ) class ProfileForm(ModelForm): if newsletter_id is not None: - newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False) - test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False) + newsletter = forms.BooleanField( + label=_("Subscribe to contest updates"), initial=False, required=False + ) + test_site = forms.BooleanField( + label=_("Enable experimental features"), initial=False, required=False + ) class Meta: model = Profile - fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script'] + fields = [ + "about", + "organizations", + "timezone", + "language", + "ace_theme", + "user_script", + ] widgets = { - 'user_script': AceWidget(theme='github'), - 'timezone': Select2Widget(attrs={'style': 'width:200px'}), - 'language': Select2Widget(attrs={'style': 'width:200px'}), - 'ace_theme': Select2Widget(attrs={'style': 'width:200px'}), + "user_script": AceWidget(theme="github"), + "timezone": Select2Widget(attrs={"style": "width:200px"}), + "language": Select2Widget(attrs={"style": "width:200px"}), + "ace_theme": Select2Widget(attrs={"style": "width:200px"}), } has_math_config = bool(settings.MATHOID_URL) if has_math_config: - fields.append('math_engine') - widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'}) + fields.append("math_engine") + widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"}) if HeavyPreviewPageDownWidget is not None: - widgets['about'] = HeavyPreviewPageDownWidget( - preview=reverse_lazy('profile_preview'), - attrs={'style': 'max-width:700px;min-width:700px;width:700px'}, + widgets["about"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("profile_preview"), + attrs={"style": "max-width:700px;min-width:700px;width:700px"}, ) def clean(self): - organizations = self.cleaned_data.get('organizations') or [] + 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 ValidationError( - _('You may not be part of more than {count} public organizations.').format(count=max_orgs)) + _( + "You may not be part of more than {count} public organizations." + ).format(count=max_orgs) + ) return self.cleaned_data def __init__(self, *args, **kwargs): - user = kwargs.pop('user', None) + user = kwargs.pop("user", None) super(ProfileForm, self).__init__(*args, **kwargs) - if not user.has_perm('judge.edit_all_organization'): - self.fields['organizations'].queryset = Organization.objects.filter( + if not user.has_perm("judge.edit_all_organization"): + self.fields["organizations"].queryset = Organization.objects.filter( Q(is_open=True) | Q(id__in=user.profile.organizations.all()), ) class ProblemSubmitForm(ModelForm): - source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True)) + source = CharField( + max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True) + ) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) def __init__(self, *args, judge_choices=(), **kwargs): super(ProblemSubmitForm, self).__init__(*args, **kwargs) - self.fields['language'].empty_label = None - self.fields['language'].label_from_instance = attrgetter('display_name') - self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct() + self.fields["language"].empty_label = None + self.fields["language"].label_from_instance = attrgetter("display_name") + self.fields["language"].queryset = Language.objects.filter( + judges__online=True + ).distinct() if judge_choices: - self.fields['judge'].widget = Select2Widget( - attrs={'style': 'width: 150px', 'data-placeholder': _('Any judge')}, + self.fields["judge"].widget = Select2Widget( + attrs={"style": "width: 150px", "data-placeholder": _("Any judge")}, ) - self.fields['judge'].choices = judge_choices + self.fields["judge"].choices = judge_choices class Meta: model = Submission - fields = ['language'] + fields = ["language"] class EditOrganizationForm(ModelForm): class Meta: model = Organization - fields = ['about', 'logo_override_image', 'admins'] - widgets = {'admins': Select2MultipleWidget()} + fields = ["about", "logo_override_image", "admins"] + widgets = {"admins": Select2MultipleWidget()} if HeavyPreviewPageDownWidget is not None: - widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview')) + widgets["about"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("organization_preview") + ) class NewMessageForm(ModelForm): class Meta: model = PrivateMessage - fields = ['title', 'content'] + fields = ["title", "content"] widgets = {} if PagedownWidget is not None: - widgets['content'] = MathJaxPagedownWidget() + widgets["content"] = MathJaxPagedownWidget() class CustomAuthenticationForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(CustomAuthenticationForm, self).__init__(*args, **kwargs) - self.fields['username'].widget.attrs.update({'placeholder': _('Username')}) - self.fields['password'].widget.attrs.update({'placeholder': _('Password')}) + self.fields["username"].widget.attrs.update({"placeholder": _("Username")}) + self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) - self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2') - self.has_facebook_auth = self._has_social_auth('FACEBOOK') - self.has_github_auth = self._has_social_auth('GITHUB_SECURE') + self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2") + self.has_facebook_auth = self._has_social_auth("FACEBOOK") + self.has_github_auth = self._has_social_auth("GITHUB_SECURE") def _has_social_auth(self, key): - return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and - getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None)) + return getattr(settings, "SOCIAL_AUTH_%s_KEY" % key, None) and getattr( + settings, "SOCIAL_AUTH_%s_SECRET" % key, None + ) class NoAutoCompleteCharField(forms.CharField): def widget_attrs(self, widget): attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget) - attrs['autocomplete'] = 'off' + attrs["autocomplete"] = "off" return attrs class TOTPForm(Form): TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES - totp_token = NoAutoCompleteCharField(validators=[ - RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')), - ]) + totp_token = NoAutoCompleteCharField( + validators=[ + RegexValidator( + "^[0-9]{6}$", + _("Two Factor Authentication tokens must be 6 decimal digits."), + ), + ] + ) def __init__(self, *args, **kwargs): - self.totp_key = kwargs.pop('totp_key') + self.totp_key = kwargs.pop("totp_key") super(TOTPForm, self).__init__(*args, **kwargs) def clean_totp_token(self): - if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE): - raise ValidationError(_('Invalid Two Factor Authentication token.')) + if not pyotp.TOTP(self.totp_key).verify( + self.cleaned_data["totp_token"], valid_window=self.TOLERANCE + ): + raise ValidationError(_("Invalid Two Factor Authentication token.")) class ProblemCloneForm(Form): - code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))]) + code = CharField( + max_length=20, + validators=[ + RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$")) + ], + ) def clean_code(self): - code = self.cleaned_data['code'] + code = self.cleaned_data["code"] if Problem.objects.filter(code=code).exists(): - raise ValidationError(_('Problem with code already exists.')) + raise ValidationError(_("Problem with code already exists.")) return code class ContestCloneForm(Form): - key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))]) + key = CharField( + max_length=20, + validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))], + ) def clean_key(self): - key = self.cleaned_data['key'] + key = self.cleaned_data["key"] if Contest.objects.filter(key=key).exists(): - raise ValidationError(_('Contest with key already exists.')) + raise ValidationError(_("Contest with key already exists.")) return key class ProblemPointsVoteForm(ModelForm): class Meta: model = ProblemPointsVote - fields = ['points'] \ No newline at end of file + fields = ["points"] diff --git a/judge/fulltext.py b/judge/fulltext.py index 5b9f7d3..209a87e 100644 --- a/judge/fulltext.py +++ b/judge/fulltext.py @@ -5,10 +5,10 @@ from django.db.models.query import QuerySet class SearchQuerySet(QuerySet): - DEFAULT = '' - BOOLEAN = ' IN BOOLEAN MODE' - NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE' - QUERY_EXPANSION = ' WITH QUERY EXPANSION' + DEFAULT = "" + BOOLEAN = " IN BOOLEAN MODE" + NATURAL_LANGUAGE = " IN NATURAL LANGUAGE MODE" + QUERY_EXPANSION = " WITH QUERY EXPANSION" def __init__(self, fields=None, **kwargs): super(SearchQuerySet, self).__init__(**kwargs) @@ -25,20 +25,26 @@ class SearchQuerySet(QuerySet): # Get the table name and column names from the model # in `table_name`.`column_name` style columns = [meta.get_field(name).column for name in self._search_fields] - full_names = ['%s.%s' % - (connection.ops.quote_name(meta.db_table), - connection.ops.quote_name(column)) - for column in columns] + full_names = [ + "%s.%s" + % ( + connection.ops.quote_name(meta.db_table), + connection.ops.quote_name(column), + ) + for column in columns + ] # Create the MATCH...AGAINST expressions - fulltext_columns = ', '.join(full_names) - match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode)) + fulltext_columns = ", ".join(full_names) + match_expr = "MATCH(%s) AGAINST (%%s%s)" % (fulltext_columns, mode) # Add the extra SELECT and WHERE options - return self.extra(select={'relevance': match_expr}, - select_params=[query], - where=[match_expr], - params=[query]) + return self.extra( + select={"relevance": match_expr}, + select_params=[query], + where=[match_expr], + params=[query], + ) class SearchManager(models.Manager): diff --git a/judge/highlight_code.py b/judge/highlight_code.py index 308a58e..ccef8d4 100644 --- a/judge/highlight_code.py +++ b/judge/highlight_code.py @@ -1,10 +1,10 @@ from django.utils.html import escape, mark_safe -__all__ = ['highlight_code'] +__all__ = ["highlight_code"] def _make_pre_code(code): - return mark_safe('
' + escape(code) + '
') + return mark_safe("
" + escape(code) + "
") def _wrap_code(inner): @@ -20,19 +20,28 @@ try: import pygments.formatters.html import pygments.util except ImportError: + def highlight_code(code, language, cssclass=None): return _make_pre_code(code) + else: + class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): def wrap(self, source, outfile): return self._wrap_div(self._wrap_pre(_wrap_code(source))) - def highlight_code(code, language, cssclass='codehilite', linenos=True): + 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) if linenos: - return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table'))) - return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass))) + return mark_safe( + pygments.highlight( + code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos="table") + ) + ) + return mark_safe( + pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)) + ) diff --git a/judge/jinja2/__init__.py b/judge/jinja2/__init__.py index 61ec4fd..93d0ed5 100644 --- a/judge/jinja2/__init__.py +++ b/judge/jinja2/__init__.py @@ -8,19 +8,33 @@ from statici18n.templatetags.statici18n import inlinei18n from judge.highlight_code import highlight_code from judge.user_translations import gettext -from . import (camo, chat, datetime, filesize, gravatar, language, markdown, rating, reference, render, social, - spaceless, submission, timedelta) +from . import ( + camo, + chat, + datetime, + filesize, + gravatar, + language, + markdown, + rating, + reference, + render, + social, + spaceless, + submission, + timedelta, +) from . import registry -registry.function('str', str) -registry.filter('str', str) -registry.filter('json', json.dumps) -registry.filter('highlight', highlight_code) -registry.filter('urlquote', urlquote) -registry.filter('roundfloat', round) -registry.function('inlinei18n', inlinei18n) -registry.function('mptt_tree', get_cached_trees) -registry.function('user_trans', gettext) +registry.function("str", str) +registry.filter("str", str) +registry.filter("json", json.dumps) +registry.filter("highlight", highlight_code) +registry.filter("urlquote", urlquote) +registry.filter("roundfloat", round) +registry.function("inlinei18n", inlinei18n) +registry.function("mptt_tree", get_cached_trees) +registry.function("user_trans", gettext) @registry.function diff --git a/judge/jinja2/camo.py b/judge/jinja2/camo.py index a60da79..1baeeb2 100644 --- a/judge/jinja2/camo.py +++ b/judge/jinja2/camo.py @@ -1,8 +1,9 @@ from judge.utils.camo import client as camo_client from . import registry + @registry.filter def camo(url): if camo_client is None: return url - return camo_client.rewrite_url(url) \ No newline at end of file + return camo_client.rewrite_url(url) diff --git a/judge/jinja2/chat.py b/judge/jinja2/chat.py index 07fec5b..564a748 100644 --- a/judge/jinja2/chat.py +++ b/judge/jinja2/chat.py @@ -1,6 +1,7 @@ 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) \ No newline at end of file + return encrypt_url(request_profile.id, profile.id) diff --git a/judge/jinja2/datetime.py b/judge/jinja2/datetime.py index 76c6bfc..eb0ec41 100644 --- a/judge/jinja2/datetime.py +++ b/judge/jinja2/datetime.py @@ -10,7 +10,7 @@ from . import registry def localtime_wrapper(func): @functools.wraps(func) def wrapper(datetime, *args, **kwargs): - if getattr(datetime, 'convert_to_local_time', True): + if getattr(datetime, "convert_to_local_time", True): datetime = localtime(datetime) return func(datetime, *args, **kwargs) @@ -22,6 +22,6 @@ registry.filter(localtime_wrapper(time)) @registry.function -@registry.render_with('widgets/relative-time.html') -def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')): - return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs} +@registry.render_with("widgets/relative-time.html") +def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("on {time}")): + return {"time": time, "format": format, "rel_format": rel, "abs_format": abs} diff --git a/judge/jinja2/filesize.py b/judge/jinja2/filesize.py index 7b27fde..0cb0fef 100644 --- a/judge/jinja2/filesize.py +++ b/judge/jinja2/filesize.py @@ -13,24 +13,28 @@ def _format_size(bytes, callback): PB = 1 << 50 if bytes < KB: - return callback('', bytes) + return callback("", bytes) elif bytes < MB: - return callback('K', bytes / KB) + return callback("K", bytes / KB) elif bytes < GB: - return callback('M', bytes / MB) + return callback("M", bytes / MB) elif bytes < TB: - return callback('G', bytes / GB) + return callback("G", bytes / GB) elif bytes < PB: - return callback('T', bytes / TB) + return callback("T", bytes / TB) else: - return callback('P', bytes / PB) + return callback("P", bytes / PB) @registry.filter def kbdetailformat(bytes): - return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x))) + return avoid_wrapping( + _format_size( + bytes * 1024, lambda x, y: ["%d %sB", "%.2f %sB"][bool(x)] % (y, x) + ) + ) @registry.filter def kbsimpleformat(kb): - return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B')) + return _format_size(kb * 1024, lambda x, y: "%.0f%s" % (y, x or "B")) diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index 35b3fb8..2848d92 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -17,9 +17,13 @@ def gravatar(email, size=80, default=None): elif isinstance(email, AbstractUser): email = email.email - gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?' - args = {'d': 'identicon', 's': str(size)} + gravatar_url = ( + "//www.gravatar.com/avatar/" + + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + + "?" + ) + args = {"d": "identicon", "s": str(size)} if default: - args['f'] = 'y' + args["f"] = "y" gravatar_url += urlencode(args) return gravatar_url diff --git a/judge/jinja2/language.py b/judge/jinja2/language.py index 344568a..dda4456 100644 --- a/judge/jinja2/language.py +++ b/judge/jinja2/language.py @@ -3,7 +3,7 @@ from django.utils import translation from . import registry -@registry.function('language_info') +@registry.function("language_info") def get_language_info(language): # ``language`` is either a language code string or a sequence # with the language code as its first item @@ -13,6 +13,6 @@ def get_language_info(language): return translation.get_language_info(str(language)) -@registry.function('language_info_list') +@registry.function("language_info_list") def get_language_info_list(langs): return [get_language_info(lang) for lang in langs] diff --git a/judge/jinja2/markdown/__init__.py b/judge/jinja2/markdown/__init__.py index 3cc5940..57af581 100644 --- a/judge/jinja2/markdown/__init__.py +++ b/judge/jinja2/markdown/__init__.py @@ -12,22 +12,28 @@ 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.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 -logger = logging.getLogger('judge.html') +logger = logging.getLogger("judge.html") NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED class CodeSafeInlineGrammar(mistune.InlineGrammar): - double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word** - emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word* + double_emphasis = re.compile(r"^\*{2}([\s\S]+?)()\*{2}(?!\*)") # **word** + emphasis = re.compile(r"^\*((?:\*\*|[^\*])+?)()\*(?!\*)") # *word* -class AwesomeInlineGrammar(MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar): +class AwesomeInlineGrammar( + MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar +): pass @@ -37,8 +43,8 @@ class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexe 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 + self.nofollow = kwargs.pop("nofollow", True) + self.texoid = TexoidRenderer() if kwargs.pop("texoid", False) else None self.parser = HTMLParser() super(AwesomeRenderer, self).__init__(*args, **kwargs) @@ -51,18 +57,18 @@ class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): else: if url.netloc and url.netloc not in NOFOLLOW_WHITELIST: return ' rel="nofollow"' - return '' + return "" def autolink(self, link, is_email=False): text = link = mistune.escape(link) if is_email: - link = 'mailto:%s' % link + link = "mailto:%s" % link return '%s' % (link, self._link_rel(link), text) def table(self, header, body): return ( '\n%s\n' - '\n%s\n
\n' + "\n%s\n\n" ) % (header, body) def link(self, link, title, text): @@ -70,40 +76,53 @@ class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): if not title: return '%s' % (link, self._link_rel(link), text) title = mistune.escape(title, quote=True) - return '%s' % (link, title, self._link_rel(link), text) + return '%s' % ( + link, + title, + self._link_rel(link), + text, + ) def block_code(self, code, lang=None): if not lang: - return '\n
%s
\n' % mistune.escape(code).rstrip() + return "\n
%s
\n" % mistune.escape(code).rstrip() return highlight_code(code, lang) def block_html(self, html): - if self.texoid and html.startswith('')] - latex = html[html.index('>') + 1:html.rindex('<')] + if self.texoid and html.startswith("")] + latex = html[html.index(">") + 1 : html.rindex("<")] latex = self.parser.unescape(latex) result = self.texoid.get_result(latex) if not result: - return '
%s
' % mistune.escape(latex, smart_amp=False) - elif 'error' not in result: - img = ('''') % { - 'svg': result['svg'], 'png': result['png'], - 'width': result['meta']['width'], 'height': result['meta']['height'], - 'tail': ' /' if self.options.get('use_xhtml') else '', + return "
%s
" % mistune.escape(latex, smart_amp=False) + elif "error" not in result: + img = ( + '''' + ) % { + "svg": result["svg"], + "png": result["png"], + "width": result["meta"]["width"], + "height": result["meta"]["height"], + "tail": " /" if self.options.get("use_xhtml") else "", } - style = ['max-width: 100%', - 'height: %s' % result['meta']['height'], - 'max-height: %s' % result['meta']['height'], - 'width: %s' % result['meta']['height']] - if 'inline' in attr: - tag = 'span' + style = [ + "max-width: 100%", + "height: %s" % result["meta"]["height"], + "max-height: %s" % result["meta"]["height"], + "width: %s" % result["meta"]["height"], + ] + if "inline" in attr: + tag = "span" else: - tag = 'div' - style += ['text-align: center'] - return '<%s style="%s">%s' % (tag, ';'.join(style), img, tag) + tag = "div" + style += ["text-align: center"] + return '<%s style="%s">%s' % (tag, ";".join(style), img, tag) else: - return '
%s
' % mistune.escape(result['error'], smart_amp=False) + return "
%s
" % mistune.escape( + result["error"], smart_amp=False + ) return super(AwesomeRenderer, self).block_html(html) def header(self, text, level, *args, **kwargs): @@ -113,30 +132,41 @@ class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer): @registry.filter def markdown(value, style, math_engine=None, lazy_load=False): styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE) - escape = styles.get('safe_mode', True) - nofollow = styles.get('nofollow', True) - texoid = TEXOID_ENABLED and styles.get('texoid', False) - math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False) + escape = styles.get("safe_mode", True) + nofollow = styles.get("nofollow", True) + texoid = TEXOID_ENABLED and styles.get("texoid", False) + math = hasattr(settings, "MATHOID_URL") and styles.get("math", False) post_processors = [] - if styles.get('use_camo', False) and camo_client is not None: + if styles.get("use_camo", False) and camo_client is not None: post_processors.append(camo_client.update_tree) if lazy_load: post_processors.append(lazy_load_processor) - renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid, - math=math and math_engine is not None, math_engine=math_engine) - markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer, - parse_block_html=1, parse_inline_html=1) + renderer = AwesomeRenderer( + escape=escape, + nofollow=nofollow, + texoid=texoid, + math=math and math_engine is not None, + math_engine=math_engine, + ) + 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)) except (XMLSyntaxError, ParserError) as e: - if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'): - logger.exception('Failed to parse HTML string') - tree = html.Element('div') + if result and ( + not isinstance(e, ParserError) or e.args[0] != "Document is empty" + ): + logger.exception("Failed to parse HTML string") + tree = html.Element("div") for processor in post_processors: processor(tree) - result = html.tostring(tree, encoding='unicode') + result = html.tostring(tree, encoding="unicode") return Markup(result) diff --git a/judge/jinja2/markdown/lazy_load.py b/judge/jinja2/markdown/lazy_load.py index cf9849c..56f5347 100644 --- a/judge/jinja2/markdown/lazy_load.py +++ b/judge/jinja2/markdown/lazy_load.py @@ -5,16 +5,16 @@ from lxml import html def lazy_load(tree): - blank = static('blank.gif') - for img in tree.xpath('.//img'): - src = img.get('src', '') - if src.startswith('data') or '-math' in img.get('class', ''): + blank = static("blank.gif") + for img in tree.xpath(".//img"): + src = img.get("src", "") + if src.startswith("data") or "-math" in img.get("class", ""): continue - noscript = html.Element('noscript') + noscript = html.Element("noscript") copy = deepcopy(img) - copy.tail = '' + copy.tail = "" noscript.append(copy) img.addprevious(noscript) - img.set('data-src', src) - img.set('src', blank) - img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil') + img.set("data-src", src) + img.set("src", blank) + img.set("class", img.get("class") + " unveil" if img.get("class") else "unveil") diff --git a/judge/jinja2/markdown/math.py b/judge/jinja2/markdown/math.py index 08883c5..1b5f4ce 100644 --- a/judge/jinja2/markdown/math.py +++ b/judge/jinja2/markdown/math.py @@ -6,13 +6,13 @@ from django.conf import settings from judge.utils.mathoid import MathoidMathParser -mistune._pre_tags.append('latex') +mistune._pre_tags.append("latex") class MathInlineGrammar(mistune.InlineGrammar): - block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL) - math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL) - text = re.compile(r'^[\s\S]+?(?=[\\%s' % (tag, extra, text, tag) + extra = m.group(2) or "" + html = "<%s%s>%s" % (tag, extra, text, tag) else: html = m.group(0) return self.renderer.inline_html(html) @@ -50,18 +52,18 @@ class MathInlineLexer(mistune.InlineLexer): class MathRenderer(mistune.Renderer): def __init__(self, *args, **kwargs): - if kwargs.pop('math', False) and settings.MATHOID_URL != False: - self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg') + if kwargs.pop("math", False) and settings.MATHOID_URL != False: + self.mathoid = MathoidMathParser(kwargs.pop("math_engine", None) or "svg") else: self.mathoid = None super(MathRenderer, self).__init__(*args, **kwargs) def block_math(self, math): if self.mathoid is None or not math: - return r'\[%s\]' % mistune.escape(str(math)) + return r"\[%s\]" % mistune.escape(str(math)) return self.mathoid.display_math(math) def math(self, math): if self.mathoid is None or not math: - return r'\(%s\)' % mistune.escape(str(math)) - return self.mathoid.inline_math(math) \ No newline at end of file + return r"\(%s\)" % mistune.escape(str(math)) + return self.mathoid.inline_math(math) diff --git a/judge/jinja2/markdown/spoiler.py b/judge/jinja2/markdown/spoiler.py index 5df7950..8717b8d 100644 --- a/judge/jinja2/markdown/spoiler.py +++ b/judge/jinja2/markdown/spoiler.py @@ -3,25 +3,28 @@ import mistune class SpoilerInlineGrammar(mistune.InlineGrammar): - spoiler = re.compile(r'^\|\|(.+?)\s+([\s\S]+?)\s*\|\|') + 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') + 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 '''
+ return """
%s
%s
-
''' % (summary, text) \ No newline at end of file +
""" % ( + summary, + text, + ) diff --git a/judge/jinja2/rating.py b/judge/jinja2/rating.py index b531bb7..3e4fc88 100644 --- a/judge/jinja2/rating.py +++ b/judge/jinja2/rating.py @@ -14,22 +14,22 @@ def _get_rating_value(func, obj): return func(obj.rating) -@registry.function('rating_class') +@registry.function("rating_class") def get_rating_class(obj): - return _get_rating_value(rating_class, obj) or 'rate-none' + return _get_rating_value(rating_class, obj) or "rate-none" -@registry.function(name='rating_name') +@registry.function(name="rating_name") def get_name(obj): - return _get_rating_value(rating_name, obj) or 'Unrated' + return _get_rating_value(rating_name, obj) or "Unrated" -@registry.function(name='rating_progress') +@registry.function(name="rating_progress") def get_progress(obj): return _get_rating_value(rating_progress, obj) or 0.0 @registry.function -@registry.render_with('user/rating.html') +@registry.render_with("user/rating.html") def rating_number(obj): - return {'rating': obj} + return {"rating": obj} diff --git a/judge/jinja2/reference.py b/judge/jinja2/reference.py index 184e109..49a6ef2 100644 --- a/judge/jinja2/reference.py +++ b/judge/jinja2/reference.py @@ -13,17 +13,17 @@ from judge.models import Contest, Problem, Profile from judge.ratings import rating_class, rating_progress from . import registry -rereference = re.compile(r'\[(r?user):(\w+)\]') +rereference = re.compile(r"\[(r?user):(\w+)\]") def get_user(username, data): if not data: - element = Element('span') + element = Element("span") element.text = username return element - element = Element('span', {'class': Profile.get_user_css_class(*data)}) - link = Element('a', {'href': reverse('user_page', args=[username])}) + element = Element("span", {"class": Profile.get_user_css_class(*data)}) + link = Element("a", {"href": reverse("user_page", args=[username])}) link.text = username element.append(link) return element @@ -31,17 +31,21 @@ def get_user(username, data): def get_user_rating(username, data): if not data: - element = Element('span') + element = Element("span") element.text = username return element rating = data[1] - element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])}) + element = Element( + "a", {"class": "rate-group", "href": reverse("user_page", args=[username])} + ) if rating: rating_css = rating_class(rating) - rate_box = Element('span', {'class': 'rate-box ' + rating_css}) - rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)})) - user = Element('span', {'class': 'rating ' + rating_css}) + rate_box = Element("span", {"class": "rate-box " + rating_css}) + rate_box.append( + Element("span", {"style": "height: %3.fem" % rating_progress(rating)}) + ) + user = Element("span", {"class": "rating " + rating_css}) user.text = username element.append(rate_box) element.append(user) @@ -51,21 +55,24 @@ def get_user_rating(username, data): def get_user_info(usernames): - return {name: (rank, rating) for name, rank, rating in - Profile.objects.filter(user__username__in=usernames) - .values_list('user__username', 'display_rank', 'rating')} + return { + name: (rank, rating) + for name, rank, rating in Profile.objects.filter( + user__username__in=usernames + ).values_list("user__username", "display_rank", "rating") + } def get_user_from_text(text): user_list = set() for i in rereference.finditer(text): - user_list.add(text[i.start() + 6: i.end() - 1]) + user_list.add(text[i.start() + 6 : i.end() - 1]) return Profile.objects.filter(user__username__in=user_list) reference_map = { - 'user': (get_user, get_user_info), - 'ruser': (get_user_rating, get_user_info), + "user": (get_user, get_user_info), + "ruser": (get_user_rating, get_user_info), } @@ -77,9 +84,9 @@ def process_reference(text): elements = [] for piece in rereference.finditer(text): if prev is None: - tail = text[last:piece.start()] + tail = text[last : piece.start()] else: - prev.append(text[last:piece.start()]) + prev.append(text[last : piece.start()]) prev = list(piece.groups()) elements.append(prev) last = piece.end() @@ -143,52 +150,52 @@ def item_title(item): return item.name elif isinstance(item, Contest): return item.name - return '' + return "" @registry.function -@registry.render_with('user/link.html') +@registry.render_with("user/link.html") def link_user(user): if isinstance(user, Profile): user, profile = user.user, user elif isinstance(user, AbstractUser): profile = user.profile - elif type(user).__name__ == 'ContestRankingProfile': + elif type(user).__name__ == "ContestRankingProfile": user, profile = user.user, user else: - raise ValueError('Expected profile or user, got %s' % (type(user),)) - return {'user': user, 'profile': profile} + raise ValueError("Expected profile or user, got %s" % (type(user),)) + return {"user": user, "profile": profile} @registry.function -@registry.render_with('user/link-list.html') +@registry.render_with("user/link-list.html") def link_users(users): - return {'users': users} + return {"users": users} @registry.function -@registry.render_with('runtime-version-fragment.html') +@registry.render_with("runtime-version-fragment.html") def runtime_versions(versions): - return {'runtime_versions': versions} + return {"runtime_versions": versions} -@registry.filter(name='absolutify') +@registry.filter(name="absolutify") def absolute_links(text, url): tree = lxml_tree.fromstring(text) - for anchor in tree.xpath('.//a'): - href = anchor.get('href') + for anchor in tree.xpath(".//a"): + href = anchor.get("href") if href: - anchor.set('href', urljoin(url, href)) + anchor.set("href", urljoin(url, href)) return tree -@registry.function(name='urljoin') +@registry.function(name="urljoin") def join(first, second, *rest): if not rest: return urljoin(first, second) return urljoin(urljoin(first, second), *rest) -@registry.filter(name='ansi2html') +@registry.filter(name="ansi2html") def ansi2html(s): return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False)) diff --git a/judge/jinja2/registry.py b/judge/jinja2/registry.py index da12166..21d3b85 100644 --- a/judge/jinja2/registry.py +++ b/judge/jinja2/registry.py @@ -5,7 +5,7 @@ tests = {} filters = {} extensions = [] -__all__ = ['render_with', 'function', 'filter', 'test', 'extension'] +__all__ = ["render_with", "function", "filter", "test", "extension"] def _store_function(store, func, name=None): @@ -16,6 +16,7 @@ def _store_function(store, func, name=None): def _register_function(store, name, func): if name is None and func is None: + def decorator(func): _store_function(store, func) return func @@ -26,6 +27,7 @@ def _register_function(store, name, func): _store_function(store, name) return name else: + def decorator(func): _store_function(store, func, name) return func diff --git a/judge/jinja2/render.py b/judge/jinja2/render.py index 778e26a..bea8c7c 100644 --- a/judge/jinja2/render.py +++ b/judge/jinja2/render.py @@ -1,5 +1,9 @@ -from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError, - VariableDoesNotExist) +from django.template import ( + Context, + Template as DjangoTemplate, + TemplateSyntaxError as DjangoTemplateSyntaxError, + VariableDoesNotExist, +) from . import registry @@ -24,4 +28,4 @@ def render_django(template, **context): try: return compile_template(template).render(Context(context)) except (VariableDoesNotExist, DjangoTemplateSyntaxError): - return 'Error rendering: %r' % template + return "Error rendering: %r" % template diff --git a/judge/jinja2/social.py b/judge/jinja2/social.py index 9f82971..645809d 100644 --- a/judge/jinja2/social.py +++ b/judge/jinja2/social.py @@ -1,13 +1,29 @@ from django.template.loader import get_template from django.utils.safestring import mark_safe -from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_gplus_url, post_to_twitter_url +from django_social_share.templatetags.social_share import ( + post_to_facebook_url, + post_to_gplus_url, + post_to_twitter_url, +) from . import registry SHARES = [ - ('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url), - ('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url), - ('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url), + ( + "post_to_twitter", + "django_social_share/templatetags/post_to_twitter.html", + post_to_twitter_url, + ), + ( + "post_to_facebook", + "django_social_share/templatetags/post_to_facebook.html", + post_to_facebook_url, + ), + ( + "post_to_gplus", + "django_social_share/templatetags/post_to_gplus.html", + post_to_gplus_url, + ), # For future versions: # ('post_to_linkedin', 'django_social_share/templatetags/post_to_linkedin.html', post_to_linkedin_url), # ('post_to_reddit', 'django_social_share/templatetags/post_to_reddit.html', post_to_reddit_url), @@ -17,7 +33,7 @@ SHARES = [ def make_func(name, template, url_func): def func(request, *args): link_text = args[-1] - context = {'request': request, 'link_text': mark_safe(link_text)} + context = {"request": request, "link_text": mark_safe(link_text)} context = url_func(context, *args[:-1]) return mark_safe(get_template(template).render(context)) @@ -31,4 +47,6 @@ for name, template, url_func in SHARES: @registry.function def recaptcha_init(language=None): - return get_template('snowpenguin/recaptcha/recaptcha_init.html').render({'explicit': False, 'language': language}) + return get_template("snowpenguin/recaptcha/recaptcha_init.html").render( + {"explicit": False, "language": language} + ) diff --git a/judge/jinja2/spaceless.py b/judge/jinja2/spaceless.py index 81c5186..0644e89 100644 --- a/judge/jinja2/spaceless.py +++ b/judge/jinja2/spaceless.py @@ -15,15 +15,17 @@ class SpacelessExtension(Extension): https://stackoverflow.com/a/23741298/1090657 """ - tags = {'spaceless'} + tags = {"spaceless"} def parse(self, parser): lineno = next(parser.stream).lineno - body = parser.parse_statements(['name:endspaceless'], drop_needle=True) + body = parser.parse_statements(["name:endspaceless"], drop_needle=True) return nodes.CallBlock( - self.call_method('_strip_spaces', [], [], None, None), - [], [], body, + self.call_method("_strip_spaces", [], [], None, None), + [], + [], + body, ).set_lineno(lineno) def _strip_spaces(self, caller=None): - return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip())) + return Markup(re.sub(r">\s+<", "><", caller().unescape().strip())) diff --git a/judge/jinja2/submission.py b/judge/jinja2/submission.py index 47765b2..bdf5650 100644 --- a/judge/jinja2/submission.py +++ b/judge/jinja2/submission.py @@ -2,7 +2,9 @@ from . import registry @registry.function -def submission_layout(submission, profile_id, user, editable_problem_ids, completed_problem_ids): +def submission_layout( + submission, profile_id, user, editable_problem_ids, completed_problem_ids +): problem_id = submission.problem_id can_view = False @@ -12,13 +14,15 @@ def submission_layout(submission, profile_id, user, editable_problem_ids, comple if profile_id == submission.user_id: can_view = True - if user.has_perm('judge.change_submission'): + if user.has_perm("judge.change_submission"): can_view = True if submission.problem_id in completed_problem_ids: - can_view |= submission.problem.is_public or profile_id in submission.problem.tester_ids + can_view |= ( + submission.problem.is_public or profile_id in submission.problem.tester_ids + ) - if not can_view and hasattr(submission, 'contest'): + if not can_view and hasattr(submission, "contest"): contest = submission.contest.participation.contest if contest.is_editable_by(user): can_view = True diff --git a/judge/jinja2/timedelta.py b/judge/jinja2/timedelta.py index 2069610..95acff6 100644 --- a/judge/jinja2/timedelta.py +++ b/judge/jinja2/timedelta.py @@ -5,14 +5,14 @@ from . import registry @registry.filter -def timedelta(value, display='long'): +def timedelta(value, display="long"): if value is None: return value return nice_repr(value, display) @registry.filter -def timestampdelta(value, display='long'): +def timestampdelta(value, display="long"): value = datetime.timedelta(seconds=value) return timedelta(value, display) @@ -23,8 +23,8 @@ def seconds(timedelta): @registry.filter -@registry.render_with('time-remaining-fragment.html') +@registry.render_with("time-remaining-fragment.html") def as_countdown(time): time_now = datetime.datetime.now(datetime.timezone.utc) initial = abs(time - time_now) - return {'countdown': time, 'initial': initial} + return {"countdown": time, "initial": initial} diff --git a/judge/judgeapi.py b/judge/judgeapi.py index 57627b2..96a2153 100644 --- a/judge/judgeapi.py +++ b/judge/judgeapi.py @@ -8,43 +8,51 @@ from django.conf import settings from judge import event_poster as event -logger = logging.getLogger('judge.judgeapi') -size_pack = struct.Struct('!I') +logger = logging.getLogger("judge.judgeapi") +size_pack = struct.Struct("!I") def _post_update_submission(submission, done=False): if submission.problem.is_public: - event.post('submissions', {'type': 'done-submission' if done else 'update-submission', - 'id': submission.id, - 'contest': submission.contest_key, - 'user': submission.user_id, 'problem': submission.problem_id, - 'status': submission.status, 'language': submission.language.key}) + event.post( + "submissions", + { + "type": "done-submission" if done else "update-submission", + "id": submission.id, + "contest": submission.contest_key, + "user": submission.user_id, + "problem": submission.problem_id, + "status": submission.status, + "language": submission.language.key, + }, + ) def judge_request(packet, reply=True): - sock = socket.create_connection(settings.BRIDGED_DJANGO_CONNECT or - settings.BRIDGED_DJANGO_ADDRESS[0]) + sock = socket.create_connection( + settings.BRIDGED_DJANGO_CONNECT or settings.BRIDGED_DJANGO_ADDRESS[0] + ) - output = json.dumps(packet, separators=(',', ':')) - output = zlib.compress(output.encode('utf-8')) - writer = sock.makefile('wb') + output = json.dumps(packet, separators=(",", ":")) + output = zlib.compress(output.encode("utf-8")) + writer = sock.makefile("wb") writer.write(size_pack.pack(len(output))) writer.write(output) writer.close() if reply: - reader = sock.makefile('rb', -1) + reader = sock.makefile("rb", -1) input = reader.read(size_pack.size) if not input: - raise ValueError('Judge did not respond') + raise ValueError("Judge did not respond") length = size_pack.unpack(input)[0] input = reader.read(length) if not input: - raise ValueError('Judge did not respond') + raise ValueError("Judge did not respond") reader.close() sock.close() - result = json.loads(zlib.decompress(input).decode('utf-8')) + result = json.loads(zlib.decompress(input).decode("utf-8")) return result @@ -56,13 +64,23 @@ def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=No REJUDGE_PRIORITY = 2 BATCH_REJUDGE_PRIORITY = 3 - updates = {'time': None, 'memory': None, 'points': None, 'result': None, 'error': None, - 'was_rejudged': rejudge, 'status': 'QU'} + updates = { + "time": None, + "memory": None, + "points": None, + "result": None, + "error": None, + "was_rejudged": rejudge, + "status": "QU", + } try: # This is set proactively; it might get unset in judgecallback's on_grading_begin if the problem doesn't # actually have pretests stored on the judge. - updates['is_pretested'] = all(ContestSubmission.objects.filter(submission=submission) - .values_list('problem__contest__run_pretests_only', 'problem__is_pretested')[0]) + updates["is_pretested"] = all( + ContestSubmission.objects.filter(submission=submission).values_list( + "problem__contest__run_pretests_only", "problem__is_pretested" + )[0] + ) except IndexError: priority = DEFAULT_PRIORITY else: @@ -76,43 +94,65 @@ def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=No # as that would prevent people from knowing a submission is being scheduled for rejudging. # It is worth noting that this mechanism does not prevent a new rejudge from being scheduled # while already queued, but that does not lead to data corruption. - if not Submission.objects.filter(id=submission.id).exclude(status__in=('P', 'G')).update(**updates): + if ( + not Submission.objects.filter(id=submission.id) + .exclude(status__in=("P", "G")) + .update(**updates) + ): return False SubmissionTestCase.objects.filter(submission_id=submission.id).delete() try: - response = judge_request({ - 'name': 'submission-request', - 'submission-id': submission.id, - 'problem-id': submission.problem.code, - 'language': submission.language.key, - 'source': submission.source.source, - 'judge-id': judge_id, - 'priority': BATCH_REJUDGE_PRIORITY if batch_rejudge else REJUDGE_PRIORITY if rejudge else priority, - }) + response = judge_request( + { + "name": "submission-request", + "submission-id": submission.id, + "problem-id": submission.problem.code, + "language": submission.language.key, + "source": submission.source.source, + "judge-id": judge_id, + "priority": BATCH_REJUDGE_PRIORITY + if batch_rejudge + else REJUDGE_PRIORITY + if rejudge + else priority, + } + ) except BaseException: - logger.exception('Failed to send request to judge') - Submission.objects.filter(id=submission.id).update(status='IE', result='IE') + logger.exception("Failed to send request to judge") + Submission.objects.filter(id=submission.id).update(status="IE", result="IE") success = False else: - if response['name'] != 'submission-received' or response['submission-id'] != submission.id: - Submission.objects.filter(id=submission.id).update(status='IE', result='IE') + if ( + response["name"] != "submission-received" + or response["submission-id"] != submission.id + ): + Submission.objects.filter(id=submission.id).update(status="IE", result="IE") _post_update_submission(submission) success = True return success def disconnect_judge(judge, force=False): - judge_request({'name': 'disconnect-judge', 'judge-id': judge.name, 'force': force}, reply=False) + judge_request( + {"name": "disconnect-judge", "judge-id": judge.name, "force": force}, + reply=False, + ) def abort_submission(submission): from .models import Submission - response = judge_request({'name': 'terminate-submission', 'submission-id': submission.id}) + + response = judge_request( + {"name": "terminate-submission", "submission-id": submission.id} + ) # This defaults to true, so that in the case the JudgeList fails to remove the submission from the queue, # and returns a bad-request, the submission is not falsely shown as "Aborted" when it will still be judged. - if not response.get('judge-aborted', True): - Submission.objects.filter(id=submission.id).update(status='AB', result='AB') - event.post('sub_%s' % Submission.get_id_secret(submission.id), {'type': 'aborted-submission'}) - _post_update_submission(submission, done=True) \ No newline at end of file + if not response.get("judge-aborted", True): + Submission.objects.filter(id=submission.id).update(status="AB", result="AB") + event.post( + "sub_%s" % Submission.get_id_secret(submission.id), + {"type": "aborted-submission"}, + ) + _post_update_submission(submission, done=True) diff --git a/judge/lxml_tree.py b/judge/lxml_tree.py index 014f7dc..bb71840 100644 --- a/judge/lxml_tree.py +++ b/judge/lxml_tree.py @@ -4,7 +4,7 @@ from django.utils.safestring import SafeData, mark_safe from lxml import html from lxml.etree import ParserError, XMLSyntaxError -logger = logging.getLogger('judge.html') +logger = logging.getLogger("judge.html") class HTMLTreeString(SafeData): @@ -12,9 +12,11 @@ class HTMLTreeString(SafeData): try: self._tree = html.fromstring(str, parser=html.HTMLParser(recover=True)) except (XMLSyntaxError, ParserError) as e: - if str and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'): - logger.exception('Failed to parse HTML string') - self._tree = html.Element('div') + if str and ( + not isinstance(e, ParserError) or e.args[0] != "Document is empty" + ): + logger.exception("Failed to parse HTML string") + self._tree = html.Element("div") def __getattr__(self, attr): try: @@ -23,15 +25,15 @@ class HTMLTreeString(SafeData): return getattr(str(self), attr) def __setattr__(self, key, value): - if key[0] == '_': + if key[0] == "_": super(HTMLTreeString, self).__setattr__(key, value) setattr(self._tree, key, value) def __repr__(self): - return '' % str(self) + return "" % str(self) def __str__(self): - return mark_safe(html.tostring(self._tree, encoding='unicode')) + return mark_safe(html.tostring(self._tree, encoding="unicode")) def __radd__(self, other): return other + str(self) diff --git a/judge/management/commands/addjudge.py b/judge/management/commands/addjudge.py index 699b3ed..f9ad589 100644 --- a/judge/management/commands/addjudge.py +++ b/judge/management/commands/addjudge.py @@ -4,15 +4,14 @@ from judge.models import Judge class Command(BaseCommand): - help = 'create a judge' + help = "create a judge" def add_arguments(self, parser): - parser.add_argument('name', help='the name of the judge') - parser.add_argument('auth_key', help='authentication key for the judge') + parser.add_argument("name", help="the name of the judge") + parser.add_argument("auth_key", help="authentication key for the judge") def handle(self, *args, **options): judge = Judge() - judge.name = options['name'] - judge.auth_key = options['auth_key'] + judge.name = options["name"] + judge.auth_key = options["auth_key"] judge.save() - diff --git a/judge/management/commands/adduser.py b/judge/management/commands/adduser.py index f214f4a..640625c 100644 --- a/judge/management/commands/adduser.py +++ b/judge/management/commands/adduser.py @@ -6,27 +6,39 @@ from judge.models import Language, Profile class Command(BaseCommand): - help = 'creates a user' + help = "creates a user" def add_arguments(self, parser): - parser.add_argument('name', help='username') - parser.add_argument('email', help='email, not necessary to be resolvable') - parser.add_argument('password', help='password for the user') - parser.add_argument('language', nargs='?', default=settings.DEFAULT_USER_LANGUAGE, - help='default language ID for user') + parser.add_argument("name", help="username") + parser.add_argument("email", help="email, not necessary to be resolvable") + parser.add_argument("password", help="password for the user") + parser.add_argument( + "language", + nargs="?", + default=settings.DEFAULT_USER_LANGUAGE, + help="default language ID for user", + ) - parser.add_argument('--superuser', action='store_true', default=False, - help="if specified, creates user with superuser privileges") - parser.add_argument('--staff', action='store_true', default=False, - help="if specified, creates user with staff privileges") + parser.add_argument( + "--superuser", + action="store_true", + default=False, + help="if specified, creates user with superuser privileges", + ) + parser.add_argument( + "--staff", + action="store_true", + default=False, + help="if specified, creates user with staff privileges", + ) def handle(self, *args, **options): - usr = User(username=options['name'], email=options['email'], is_active=True) - usr.set_password(options['password']) - usr.is_superuser = options['superuser'] - usr.is_staff = options['staff'] + usr = User(username=options["name"], email=options["email"], is_active=True) + usr.set_password(options["password"]) + usr.is_superuser = options["superuser"] + usr.is_staff = options["staff"] usr.save() profile = Profile(user=usr) - profile.language = Language.objects.get(key=options['language']) + profile.language = Language.objects.get(key=options["language"]) profile.save() diff --git a/judge/management/commands/camo.py b/judge/management/commands/camo.py index aa65521..774837a 100644 --- a/judge/management/commands/camo.py +++ b/judge/management/commands/camo.py @@ -4,13 +4,13 @@ from judge.utils.camo import client as camo_client class Command(BaseCommand): - help = 'obtains the camo url for the specified url' + help = "obtains the camo url for the specified url" def add_arguments(self, parser): - parser.add_argument('url', help='url to use camo on') + parser.add_argument("url", help="url to use camo on") def handle(self, *args, **options): if camo_client is None: - raise CommandError('Camo not available') + raise CommandError("Camo not available") - print(camo_client.image_url(options['url'])) + print(camo_client.image_url(options["url"])) diff --git a/judge/management/commands/copy_language.py b/judge/management/commands/copy_language.py index 233ccc7..09120a6 100644 --- a/judge/management/commands/copy_language.py +++ b/judge/management/commands/copy_language.py @@ -4,24 +4,30 @@ from judge.models import Language, LanguageLimit class Command(BaseCommand): - help = 'allows the problems that allow to be submitted in ' + help = "allows the problems that allow to be submitted in " def add_arguments(self, parser): - parser.add_argument('source', help='language to copy from') - parser.add_argument('target', help='language to copy to') + parser.add_argument("source", help="language to copy from") + parser.add_argument("target", help="language to copy to") def handle(self, *args, **options): try: - source = Language.objects.get(key=options['source']) + source = Language.objects.get(key=options["source"]) except Language.DoesNotExist: - raise CommandError('Invalid source language: %s' % options['source']) + raise CommandError("Invalid source language: %s" % options["source"]) try: - target = Language.objects.get(key=options['target']) + target = Language.objects.get(key=options["target"]) except Language.DoesNotExist: - raise CommandError('Invalid target language: %s' % options['target']) + raise CommandError("Invalid target language: %s" % options["target"]) target.problem_set.set(source.problem_set.all()) - LanguageLimit.objects.bulk_create(LanguageLimit(problem=ll.problem, language=target, time_limit=ll.time_limit, - memory_limit=ll.memory_limit) - for ll in LanguageLimit.objects.filter(language=source)) + LanguageLimit.objects.bulk_create( + LanguageLimit( + problem=ll.problem, + language=target, + time_limit=ll.time_limit, + memory_limit=ll.memory_limit, + ) + for ll in LanguageLimit.objects.filter(language=source) + ) diff --git a/judge/management/commands/create_problem.py b/judge/management/commands/create_problem.py index b095e08..568103f 100644 --- a/judge/management/commands/create_problem.py +++ b/judge/management/commands/create_problem.py @@ -4,20 +4,20 @@ from judge.models import Problem, ProblemGroup, ProblemType class Command(BaseCommand): - help = 'create an empty problem' + help = "create an empty problem" def add_arguments(self, parser): - parser.add_argument('code', help='problem code') - parser.add_argument('name', help='problem title') - parser.add_argument('body', help='problem description') - parser.add_argument('type', help='problem type') - parser.add_argument('group', help='problem group') + parser.add_argument("code", help="problem code") + parser.add_argument("name", help="problem title") + parser.add_argument("body", help="problem description") + parser.add_argument("type", help="problem type") + parser.add_argument("group", help="problem group") def handle(self, *args, **options): problem = Problem() - problem.code = options['code'] - problem.name = options['name'] - problem.description = options['body'] - problem.group = ProblemGroup.objects.get(name=options['group']) - problem.types = [ProblemType.objects.get(name=options['type'])] + problem.code = options["code"] + problem.name = options["name"] + problem.description = options["body"] + problem.group = ProblemGroup.objects.get(name=options["group"]) + problem.types = [ProblemType.objects.get(name=options["type"])] problem.save() diff --git a/judge/management/commands/generate_data.py b/judge/management/commands/generate_data.py index f7fdbd4..687ccd1 100644 --- a/judge/management/commands/generate_data.py +++ b/judge/management/commands/generate_data.py @@ -7,43 +7,47 @@ from django.conf import settings def gen_submissions(): - headers = ['uid', 'pid'] - with open(os.path.join(settings.ML_DATA_PATH, 'submissions.csv'), 'w') as csvfile: + headers = ["uid", "pid"] + with open(os.path.join(settings.ML_DATA_PATH, "submissions.csv"), "w") as csvfile: f = csv.writer(csvfile) f.writerow(headers) - + last_pid = defaultdict(int) for u in Profile.objects.all(): used = set() - print('Processing user', u.id) - for s in Submission.objects.filter(user=u).order_by('-date'): + print("Processing user", u.id) + for s in Submission.objects.filter(user=u).order_by("-date"): if s.problem.id not in used: used.add(s.problem.id) f.writerow([u.id, s.problem.id]) + def gen_users(): - headers = ['uid', 'username', 'rating', 'points'] - with open(os.path.join(settings.ML_DATA_PATH, 'profiles.csv'), 'w') as csvfile: + headers = ["uid", "username", "rating", "points"] + with open(os.path.join(settings.ML_DATA_PATH, "profiles.csv"), "w") as csvfile: f = csv.writer(csvfile) f.writerow(headers) - + for u in Profile.objects.all(): f.writerow([u.id, u.username, u.rating, u.performance_points]) + def gen_problems(): - headers = ['pid', 'code', 'name', 'points', 'url'] - with open(os.path.join(settings.ML_DATA_PATH, 'problems.csv'), 'w') as csvfile: + headers = ["pid", "code", "name", "points", "url"] + with open(os.path.join(settings.ML_DATA_PATH, "problems.csv"), "w") as csvfile: f = csv.writer(csvfile) f.writerow(headers) - + for p in Problem.objects.all(): - f.writerow([p.id, p.code, p.name, p.points, 'lqdoj.edu.vn/problem/' + p.code]) + f.writerow( + [p.id, p.code, p.name, p.points, "lqdoj.edu.vn/problem/" + p.code] + ) class Command(BaseCommand): - help = 'generate data for ML' + help = "generate data for ML" def handle(self, *args, **options): gen_users() gen_problems() - gen_submissions() \ No newline at end of file + gen_submissions() diff --git a/judge/management/commands/makedmojmessages.py b/judge/management/commands/makedmojmessages.py index 55d3acc..981726f 100644 --- a/judge/management/commands/makedmojmessages.py +++ b/judge/management/commands/makedmojmessages.py @@ -5,33 +5,69 @@ import sys from django.conf import settings from django.core.management import CommandError -from django.core.management.commands.makemessages import Command as MakeMessagesCommand, check_programs +from django.core.management.commands.makemessages import ( + Command as MakeMessagesCommand, + check_programs, +) from judge.models import NavigationBar, ProblemType class Command(MakeMessagesCommand): def add_arguments(self, parser): - parser.add_argument('--locale', '-l', default=[], dest='locale', action='append', - help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). ' - 'Can be used multiple times.') - parser.add_argument('--exclude', '-x', default=[], dest='exclude', action='append', - help='Locales to exclude. Default is none. Can be used multiple times.') - parser.add_argument('--all', '-a', action='store_true', dest='all', - default=False, help='Updates the message files for all existing locales.') - parser.add_argument('--no-wrap', action='store_true', dest='no_wrap', - default=False, help="Don't break long message lines into several lines.") - parser.add_argument('--no-obsolete', action='store_true', dest='no_obsolete', - default=False, help="Remove obsolete message strings.") - parser.add_argument('--keep-pot', action='store_true', dest='keep_pot', - default=False, help="Keep .pot file after making messages. Useful when debugging.") + parser.add_argument( + "--locale", + "-l", + default=[], + dest="locale", + action="append", + help="Creates or updates the message files for the given locale(s) (e.g. pt_BR). " + "Can be used multiple times.", + ) + parser.add_argument( + "--exclude", + "-x", + default=[], + dest="exclude", + action="append", + help="Locales to exclude. Default is none. Can be used multiple times.", + ) + parser.add_argument( + "--all", + "-a", + action="store_true", + dest="all", + default=False, + help="Updates the message files for all existing locales.", + ) + parser.add_argument( + "--no-wrap", + action="store_true", + dest="no_wrap", + default=False, + help="Don't break long message lines into several lines.", + ) + parser.add_argument( + "--no-obsolete", + action="store_true", + dest="no_obsolete", + default=False, + help="Remove obsolete message strings.", + ) + parser.add_argument( + "--keep-pot", + action="store_true", + dest="keep_pot", + default=False, + help="Keep .pot file after making messages. Useful when debugging.", + ) def handle(self, *args, **options): - locale = options.get('locale') - exclude = options.get('exclude') - self.domain = 'dmoj-user' - self.verbosity = options.get('verbosity') - process_all = options.get('all') + locale = options.get("locale") + exclude = options.get("exclude") + self.domain = "dmoj-user" + self.verbosity = options.get("verbosity") + process_all = options.get("all") # Need to ensure that the i18n framework is enabled if settings.configured: @@ -40,43 +76,47 @@ class Command(MakeMessagesCommand): settings.configure(USE_I18N=True) # Avoid messing with mutable class variables - if options.get('no_wrap'): - self.msgmerge_options = self.msgmerge_options[:] + ['--no-wrap'] - self.msguniq_options = self.msguniq_options[:] + ['--no-wrap'] - self.msgattrib_options = self.msgattrib_options[:] + ['--no-wrap'] - self.xgettext_options = self.xgettext_options[:] + ['--no-wrap'] - if options.get('no_location'): - self.msgmerge_options = self.msgmerge_options[:] + ['--no-location'] - self.msguniq_options = self.msguniq_options[:] + ['--no-location'] - self.msgattrib_options = self.msgattrib_options[:] + ['--no-location'] - self.xgettext_options = self.xgettext_options[:] + ['--no-location'] + if options.get("no_wrap"): + self.msgmerge_options = self.msgmerge_options[:] + ["--no-wrap"] + self.msguniq_options = self.msguniq_options[:] + ["--no-wrap"] + self.msgattrib_options = self.msgattrib_options[:] + ["--no-wrap"] + self.xgettext_options = self.xgettext_options[:] + ["--no-wrap"] + if options.get("no_location"): + self.msgmerge_options = self.msgmerge_options[:] + ["--no-location"] + self.msguniq_options = self.msguniq_options[:] + ["--no-location"] + self.msgattrib_options = self.msgattrib_options[:] + ["--no-location"] + self.xgettext_options = self.xgettext_options[:] + ["--no-location"] - self.no_obsolete = options.get('no_obsolete') - self.keep_pot = options.get('keep_pot') + self.no_obsolete = options.get("no_obsolete") + self.keep_pot = options.get("keep_pot") if locale is None and not exclude and not process_all: - raise CommandError("Type '%s help %s' for usage information." % ( - os.path.basename(sys.argv[0]), sys.argv[1])) + raise CommandError( + "Type '%s help %s' for usage information." + % (os.path.basename(sys.argv[0]), sys.argv[1]) + ) self.invoked_for_django = False self.locale_paths = [] self.default_locale_path = None - if os.path.isdir(os.path.join('conf', 'locale')): - self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))] + if os.path.isdir(os.path.join("conf", "locale")): + self.locale_paths = [os.path.abspath(os.path.join("conf", "locale"))] self.default_locale_path = self.locale_paths[0] self.invoked_for_django = True else: self.locale_paths.extend(settings.LOCALE_PATHS) # Allow to run makemessages inside an app dir - if os.path.isdir('locale'): - self.locale_paths.append(os.path.abspath('locale')) + if os.path.isdir("locale"): + self.locale_paths.append(os.path.abspath("locale")) if self.locale_paths: self.default_locale_path = self.locale_paths[0] if not os.path.exists(self.default_locale_path): os.makedirs(self.default_locale_path) # Build locale list - locale_dirs = list(filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))) + locale_dirs = list( + filter(os.path.isdir, glob.glob("%s/*" % self.default_locale_path)) + ) all_locales = list(map(os.path.basename, locale_dirs)) # Account for excluded locales @@ -87,9 +127,9 @@ class Command(MakeMessagesCommand): locales = set(locales) - set(exclude) if locales: - check_programs('msguniq', 'msgmerge', 'msgattrib') + check_programs("msguniq", "msgmerge", "msgattrib") - check_programs('xgettext') + check_programs("xgettext") try: potfiles = self.build_potfiles() @@ -108,23 +148,33 @@ class Command(MakeMessagesCommand): return [] def _emit_message(self, potfile, string): - potfile.write(''' + potfile.write( + """ msgid "%s" msgstr "" -''' % string.replace('\\', r'\\').replace('\t', '\\t').replace('\n', '\\n').replace('"', '\\"')) +""" + % string.replace("\\", r"\\") + .replace("\t", "\\t") + .replace("\n", "\\n") + .replace('"', '\\"') + ) def process_files(self, file_list): - with io.open(os.path.join(self.default_locale_path, 'dmoj-user.pot'), 'w', encoding='utf-8') as potfile: + with io.open( + os.path.join(self.default_locale_path, "dmoj-user.pot"), + "w", + encoding="utf-8", + ) as potfile: if self.verbosity > 1: - self.stdout.write('processing navigation bar') - for label in NavigationBar.objects.values_list('label', flat=True): + self.stdout.write("processing navigation bar") + for label in NavigationBar.objects.values_list("label", flat=True): if self.verbosity > 2: self.stdout.write('processing navigation item label "%s"\n' % label) self._emit_message(potfile, label) if self.verbosity > 1: - self.stdout.write('processing problem types') - for name in ProblemType.objects.values_list('full_name', flat=True): + self.stdout.write("processing problem types") + for name in ProblemType.objects.values_list("full_name", flat=True): if self.verbosity > 2: self.stdout.write('processing problem type name "%s"\n' % name) self._emit_message(potfile, name) diff --git a/judge/management/commands/render_pdf.py b/judge/management/commands/render_pdf.py index 258081d..f8b2966 100644 --- a/judge/management/commands/render_pdf.py +++ b/judge/management/commands/render_pdf.py @@ -8,52 +8,98 @@ 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, SeleniumPDFRender, \ - SlimerJSPdfMaker +from judge.pdf_problems import ( + DefaultPdfMaker, + PhantomJSPdfMaker, + PuppeteerPDFRender, + SeleniumPDFRender, + SlimerJSPdfMaker, +) + class Command(BaseCommand): - help = 'renders a PDF file of a problem' + help = "renders a PDF file of a problem" def add_arguments(self, parser): - parser.add_argument('code', help='code of problem to render') - parser.add_argument('directory', nargs='?', help='directory to store temporaries') - parser.add_argument('-l', '--language', default=settings.LANGUAGE_CODE, - help='language to render PDF in') - parser.add_argument('-p', '--phantomjs', action='store_const', const=PhantomJSPdfMaker, - default=DefaultPdfMaker, dest='engine') - 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') + parser.add_argument("code", help="code of problem to render") + parser.add_argument( + "directory", nargs="?", help="directory to store temporaries" + ) + parser.add_argument( + "-l", + "--language", + default=settings.LANGUAGE_CODE, + help="language to render PDF in", + ) + parser.add_argument( + "-p", + "--phantomjs", + action="store_const", + const=PhantomJSPdfMaker, + default=DefaultPdfMaker, + dest="engine", + ) + 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: - problem = Problem.objects.get(code=options['code']) + problem = Problem.objects.get(code=options["code"]) except Problem.DoesNotExist: - print('Bad problem code') + print("Bad problem code") return try: - trans = problem.translations.get(language=options['language']) + trans = problem.translations.get(language=options["language"]) except ProblemTranslation.DoesNotExist: trans = None - directory = options['directory'] - with options['engine'](directory, clean_up=directory is None) as maker, \ - translation.override(options['language']): + directory = options["directory"] + with options["engine"]( + directory, clean_up=directory is None + ) as maker, translation.override(options["language"]): problem_name = problem.name if trans is None else trans.name - maker.html = get_template('problem/raw.html').render({ - 'problem': problem, - 'problem_name': problem_name, - 'description': problem.description if trans is None else trans.description, - 'url': '', - 'math_engine': maker.math_engine, - }).replace('"//', '"https://').replace("'//", "'https://") + maker.html = ( + get_template("problem/raw.html") + .render( + { + "problem": problem, + "problem_name": problem_name, + "description": problem.description + if trans is None + else trans.description, + "url": "", + "math_engine": maker.math_engine, + } + ) + .replace('"//', '"https://') + .replace("'//", "'https://") + ) maker.title = problem_name - for file in ('style.css', 'pygment-github.css', 'mathjax_config.js'): + for file in ("style.css", "pygment-github.css", "mathjax_config.js"): maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.make(debug=True) if not maker.success: print(maker.log, file=sys.stderr) elif directory is None: - shutil.move(maker.pdffile, problem.code + '.pdf') + shutil.move(maker.pdffile, problem.code + ".pdf") diff --git a/judge/management/commands/runbridged.py b/judge/management/commands/runbridged.py index c6f4536..b968d63 100644 --- a/judge/management/commands/runbridged.py +++ b/judge/management/commands/runbridged.py @@ -5,4 +5,4 @@ from judge.bridge.daemon import judge_daemon class Command(BaseCommand): def handle(self, *args, **options): - judge_daemon() \ No newline at end of file + judge_daemon() diff --git a/judge/management/commands/runmoss.py b/judge/management/commands/runmoss.py index 0558da3..c8d026c 100644 --- a/judge/management/commands/runmoss.py +++ b/judge/management/commands/runmoss.py @@ -6,42 +6,50 @@ from judge.models import Contest, ContestParticipation, Submission class Command(BaseCommand): - help = 'Checks for duplicate code using MOSS' + help = "Checks for duplicate code using MOSS" LANG_MAPPING = { - ('C++', MOSS_LANG_CC), - ('C', MOSS_LANG_C), - ('Java', MOSS_LANG_JAVA), - ('Python', MOSS_LANG_PYTHON), - ('Pascal', MOSS_LANG_PASCAL), + ("C++", MOSS_LANG_CC), + ("C", MOSS_LANG_C), + ("Java", MOSS_LANG_JAVA), + ("Python", MOSS_LANG_PYTHON), + ("Pascal", MOSS_LANG_PASCAL), } def add_arguments(self, parser): - parser.add_argument('contest', help='the id of the contest') + parser.add_argument("contest", help="the id of the contest") def handle(self, *args, **options): moss_api_key = settings.MOSS_API_KEY if moss_api_key is None: - print('No MOSS API Key supplied') + print("No MOSS API Key supplied") return - contest = options['contest'] + contest = options["contest"] - for problem in Contest.objects.get(key=contest).problems.order_by('code'): - print('========== %s / %s ==========' % (problem.code, problem.name)) + for problem in Contest.objects.get(key=contest).problems.order_by("code"): + print("========== %s / %s ==========" % (problem.code, problem.name)) for dmoj_lang, moss_lang in self.LANG_MAPPING: - print("%s: " % dmoj_lang, end=' ') + print("%s: " % dmoj_lang, end=" ") subs = Submission.objects.filter( - contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE), + contest__participation__virtual__in=( + ContestParticipation.LIVE, + ContestParticipation.SPECTATE, + ), contest__participation__contest__key=contest, - result='AC', problem__id=problem.id, + result="AC", + problem__id=problem.id, language__common_name=dmoj_lang, - ).values_list('user__user__username', 'source__source') + ).values_list("user__user__username", "source__source") if not subs: - print('') + print("") continue - moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100, - comment='%s - %s' % (contest, problem.code)) + moss_call = MOSS( + moss_api_key, + language=moss_lang, + matching_file_limit=100, + comment="%s - %s" % (contest, problem.code), + ) users = set() @@ -49,6 +57,6 @@ class Command(BaseCommand): if username in users: continue users.add(username) - moss_call.add_file_from_memory(username, source.encode('utf-8')) + moss_call.add_file_from_memory(username, source.encode("utf-8")) - print('(%d): %s' % (subs.count(), moss_call.process())) + print("(%d): %s" % (subs.count(), moss_call.process())) diff --git a/judge/middleware.py b/judge/middleware.py index b35e1a3..6f5a053 100644 --- a/judge/middleware.py +++ b/judge/middleware.py @@ -10,11 +10,13 @@ class ShortCircuitMiddleware: def __call__(self, request): try: - callback, args, kwargs = resolve(request.path_info, getattr(request, 'urlconf', None)) + callback, args, kwargs = resolve( + request.path_info, getattr(request, "urlconf", None) + ) except Resolver404: callback, args, kwargs = None, None, None - if getattr(callback, 'short_circuit_middleware', False): + if getattr(callback, "short_circuit_middleware", False): return callback(request, *args, **kwargs) return self.get_response(request) @@ -26,11 +28,16 @@ class DMOJLoginMiddleware(object): def __call__(self, request): if request.user.is_authenticated: profile = request.profile = request.user.profile - login_2fa_path = reverse('login_2fa') - if (profile.is_totp_enabled and not request.session.get('2fa_passed', False) and - request.path not in (login_2fa_path, reverse('auth_logout')) and - not request.path.startswith(settings.STATIC_URL)): - return HttpResponseRedirect(login_2fa_path + '?next=' + urlquote(request.get_full_path())) + login_2fa_path = reverse("login_2fa") + if ( + profile.is_totp_enabled + and not request.session.get("2fa_passed", False) + and request.path not in (login_2fa_path, reverse("auth_logout")) + and not request.path.startswith(settings.STATIC_URL) + ): + return HttpResponseRedirect( + login_2fa_path + "?next=" + urlquote(request.get_full_path()) + ) else: request.profile = None return self.get_response(request) @@ -57,7 +64,7 @@ class ContestMiddleware(object): profile.update_contest() request.participation = profile.current_contest request.in_contest = request.participation is not None - request.contest_mode = request.session.get('contest_mode', True) + request.contest_mode = request.session.get("contest_mode", True) else: request.in_contest = False request.participation = None diff --git a/judge/migrations/0001_squashed_0084_contest_formats.py b/judge/migrations/0001_squashed_0084_contest_formats.py index 22aa659..ae4e6ae 100644 --- a/judge/migrations/0001_squashed_0084_contest_formats.py +++ b/judge/migrations/0001_squashed_0084_contest_formats.py @@ -14,748 +14,3275 @@ import judge.utils.problem_data class Migration(migrations.Migration): - replaces = [('judge', '0001_initial'), ('judge', '0002_license'), ('judge', '0003_license_key'), ('judge', '0004_language_limit'), ('judge', '0005_nav_path_len'), ('judge', '0006_language_extension'), ('judge', '0007_test_site_perm'), ('judge', '0008_contestproblem_order'), ('judge', '0009_solution_problem'), ('judge', '0010_comment_page_index'), ('judge', '0011_organization_is_open'), ('judge', '0012_organization_perms'), ('judge', '0013_private_contests'), ('judge', '0014_multi_organization'), ('judge', '0015_remove_single_organization'), ('judge', '0016_organizationrequest'), ('judge', '0017_edit_public_problem_perm'), ('judge', '0018_django_1_9'), ('judge', '0019_og_images'), ('judge', '0020_profile_user_script'), ('judge', '0021_output_prefix_override'), ('judge', '0022_judge_last_ping'), ('judge', '0023_contest_tag'), ('judge', '0024_submission_judge'), ('judge', '0025_submission_rejudge_flag'), ('judge', '0026_change_public_visibility_perm'), ('judge', '0027_bridge_revert'), ('judge', '0028_judge_ip'), ('judge', '0029_problem_translation'), ('judge', '0030_remove_contest_profile'), ('judge', '0031_judge_versions'), ('judge', '0032_hide_problem_tags_in_contest'), ('judge', '0033_proper_pretest_support'), ('judge', '0034_submission_is_pretested'), ('judge', '0035_contest_spectate_mode'), ('judge', '0036_contest_participation_unique'), ('judge', '0037_user_count_ac_rate_field'), ('judge', '0038_profile_problem_count'), ('judge', '0039_remove_contest_is_external'), ('judge', '0040_profile_math_engine'), ('judge', '0041_virtual_contest_participation'), ('judge', '0042_remove_spectate_field'), ('judge', '0043_contest_user_count'), ('judge', '0044_organization_slots'), ('judge', '0045_organization_access_code'), ('judge', '0046_blogpost_authors'), ('judge', '0047_site_managed_data'), ('judge', '0048_site_managed_checkers'), ('judge', '0049_contest_summary'), ('judge', '0050_problem_tester_field'), ('judge', '0051_was_rejudged_field'), ('judge', '0052_switch_to_durationfield'), ('judge', '0053_opengraph_problems'), ('judge', '0054_tickets'), ('judge', '0055_add_performance_points'), ('judge', '0056_ticket_is_open'), ('judge', '0057_blue_pretests'), ('judge', '0058_problem_curator_field'), ('judge', '0059_problem_is_manually_managed'), ('judge', '0060_contest_clarifications'), ('judge', '0061_language_template'), ('judge', '0062_add_contest_submission_limit'), ('judge', '0063_new_solutions'), ('judge', '0064_unique_solution'), ('judge', '0065_blogpost_perms'), ('judge', '0066_submission_date_index'), ('judge', '0067_contest_access_code'), ('judge', '0068_hide_scoreboard'), ('judge', '0069_judge_blocking'), ('judge', '0070_organization_slug'), ('judge', '0071_organization_private_problems'), ('judge', '0072_contest_logo_override_image'), ('judge', '0073_comment_lock'), ('judge', '0074_totp'), ('judge', '0075_organization_admin_reverse'), ('judge', '0076_problem_statistics'), ('judge', '0077_remove_organization_key'), ('judge', '0078_add_user_notes'), ('judge', '0079_remove_comment_title'), ('judge', '0080_contest_banned_users'), ('judge', '0081_unlisted_users'), ('judge', '0082_remove_profile_name'), ('judge', '0083_extended_feedback'), ('judge', '0084_contest_formats')] + replaces = [ + ("judge", "0001_initial"), + ("judge", "0002_license"), + ("judge", "0003_license_key"), + ("judge", "0004_language_limit"), + ("judge", "0005_nav_path_len"), + ("judge", "0006_language_extension"), + ("judge", "0007_test_site_perm"), + ("judge", "0008_contestproblem_order"), + ("judge", "0009_solution_problem"), + ("judge", "0010_comment_page_index"), + ("judge", "0011_organization_is_open"), + ("judge", "0012_organization_perms"), + ("judge", "0013_private_contests"), + ("judge", "0014_multi_organization"), + ("judge", "0015_remove_single_organization"), + ("judge", "0016_organizationrequest"), + ("judge", "0017_edit_public_problem_perm"), + ("judge", "0018_django_1_9"), + ("judge", "0019_og_images"), + ("judge", "0020_profile_user_script"), + ("judge", "0021_output_prefix_override"), + ("judge", "0022_judge_last_ping"), + ("judge", "0023_contest_tag"), + ("judge", "0024_submission_judge"), + ("judge", "0025_submission_rejudge_flag"), + ("judge", "0026_change_public_visibility_perm"), + ("judge", "0027_bridge_revert"), + ("judge", "0028_judge_ip"), + ("judge", "0029_problem_translation"), + ("judge", "0030_remove_contest_profile"), + ("judge", "0031_judge_versions"), + ("judge", "0032_hide_problem_tags_in_contest"), + ("judge", "0033_proper_pretest_support"), + ("judge", "0034_submission_is_pretested"), + ("judge", "0035_contest_spectate_mode"), + ("judge", "0036_contest_participation_unique"), + ("judge", "0037_user_count_ac_rate_field"), + ("judge", "0038_profile_problem_count"), + ("judge", "0039_remove_contest_is_external"), + ("judge", "0040_profile_math_engine"), + ("judge", "0041_virtual_contest_participation"), + ("judge", "0042_remove_spectate_field"), + ("judge", "0043_contest_user_count"), + ("judge", "0044_organization_slots"), + ("judge", "0045_organization_access_code"), + ("judge", "0046_blogpost_authors"), + ("judge", "0047_site_managed_data"), + ("judge", "0048_site_managed_checkers"), + ("judge", "0049_contest_summary"), + ("judge", "0050_problem_tester_field"), + ("judge", "0051_was_rejudged_field"), + ("judge", "0052_switch_to_durationfield"), + ("judge", "0053_opengraph_problems"), + ("judge", "0054_tickets"), + ("judge", "0055_add_performance_points"), + ("judge", "0056_ticket_is_open"), + ("judge", "0057_blue_pretests"), + ("judge", "0058_problem_curator_field"), + ("judge", "0059_problem_is_manually_managed"), + ("judge", "0060_contest_clarifications"), + ("judge", "0061_language_template"), + ("judge", "0062_add_contest_submission_limit"), + ("judge", "0063_new_solutions"), + ("judge", "0064_unique_solution"), + ("judge", "0065_blogpost_perms"), + ("judge", "0066_submission_date_index"), + ("judge", "0067_contest_access_code"), + ("judge", "0068_hide_scoreboard"), + ("judge", "0069_judge_blocking"), + ("judge", "0070_organization_slug"), + ("judge", "0071_organization_private_problems"), + ("judge", "0072_contest_logo_override_image"), + ("judge", "0073_comment_lock"), + ("judge", "0074_totp"), + ("judge", "0075_organization_admin_reverse"), + ("judge", "0076_problem_statistics"), + ("judge", "0077_remove_organization_key"), + ("judge", "0078_add_user_notes"), + ("judge", "0079_remove_comment_title"), + ("judge", "0080_contest_banned_users"), + ("judge", "0081_unlisted_users"), + ("judge", "0082_remove_profile_name"), + ("judge", "0083_extended_feedback"), + ("judge", "0084_contest_formats"), + ] initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0002_remove_content_type_name'), + ("contenttypes", "0002_remove_content_type_name"), ] operations = [ migrations.CreateModel( - name='BlogPost', + name="BlogPost", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='post title')), - ('slug', models.SlugField(verbose_name='slug')), - ('visible', models.BooleanField(default=False, verbose_name='public visibility')), - ('sticky', models.BooleanField(default=False, verbose_name='sticky')), - ('publish_on', models.DateTimeField(verbose_name='publish after')), - ('content', models.TextField(verbose_name='post content')), - ('summary', models.TextField(blank=True, verbose_name='post summary')), - ('og_image', models.CharField(blank=True, default='', max_length=150, verbose_name='openGraph image')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100, verbose_name="post title")), + ("slug", models.SlugField(verbose_name="slug")), + ( + "visible", + models.BooleanField( + default=False, verbose_name="public visibility" + ), + ), + ("sticky", models.BooleanField(default=False, verbose_name="sticky")), + ("publish_on", models.DateTimeField(verbose_name="publish after")), + ("content", models.TextField(verbose_name="post content")), + ("summary", models.TextField(blank=True, verbose_name="post summary")), + ( + "og_image", + models.CharField( + blank=True, + default="", + max_length=150, + verbose_name="openGraph image", + ), + ), ], options={ - 'verbose_name_plural': 'blog posts', - 'permissions': (('edit_all_post', 'Edit all posts'),), - 'verbose_name': 'blog post', + "verbose_name_plural": "blog posts", + "permissions": (("edit_all_post", "Edit all posts"),), + "verbose_name": "blog post", }, ), migrations.CreateModel( - name='Comment', + name="Comment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')), - ('page', models.CharField(db_index=True, max_length=30, validators=[django.core.validators.RegexValidator('^[pcs]:[a-z0-9]+$|^b:\\d+$', 'Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$')], verbose_name='associated page')), - ('score', models.IntegerField(default=0, verbose_name='votes')), - ('body', models.TextField(max_length=8192, verbose_name='body of comment')), - ('hidden', models.BooleanField(default=0, verbose_name='hide the comment')), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField(auto_now_add=True, verbose_name="posted time"), + ), + ( + "page", + models.CharField( + db_index=True, + max_length=30, + validators=[ + django.core.validators.RegexValidator( + "^[pcs]:[a-z0-9]+$|^b:\\d+$", + "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$", + ) + ], + verbose_name="associated page", + ), + ), + ("score", models.IntegerField(default=0, verbose_name="votes")), + ( + "body", + models.TextField(max_length=8192, verbose_name="body of comment"), + ), + ( + "hidden", + models.BooleanField(default=0, verbose_name="hide the comment"), + ), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), ], options={ - 'verbose_name_plural': 'comments', - 'verbose_name': 'comment', + "verbose_name_plural": "comments", + "verbose_name": "comment", }, ), migrations.CreateModel( - name='CommentLock', + name="CommentLock", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('page', models.CharField(db_index=True, max_length=30, validators=[django.core.validators.RegexValidator('^[pcs]:[a-z0-9]+$|^b:\\d+$', 'Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$')], verbose_name='associated page')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "page", + models.CharField( + db_index=True, + max_length=30, + validators=[ + django.core.validators.RegexValidator( + "^[pcs]:[a-z0-9]+$|^b:\\d+$", + "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$", + ) + ], + verbose_name="associated page", + ), + ), ], options={ - 'permissions': (('override_comment_lock', 'Override comment lock'),), + "permissions": (("override_comment_lock", "Override comment lock"),), }, ), migrations.CreateModel( - name='CommentVote', + name="CommentVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('score', models.IntegerField()), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='judge.Comment')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("score", models.IntegerField()), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="votes", + to="judge.Comment", + ), + ), ], options={ - 'verbose_name_plural': 'comment votes', - 'verbose_name': 'comment vote', + "verbose_name_plural": "comment votes", + "verbose_name": "comment vote", }, ), migrations.CreateModel( - name='Contest', + name="Contest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Contest id must be ^[a-z0-9]+$')], verbose_name='contest id')), - ('name', models.CharField(db_index=True, max_length=100, verbose_name='contest name')), - ('description', models.TextField(blank=True, verbose_name='description')), - ('start_time', models.DateTimeField(db_index=True, verbose_name='start time')), - ('end_time', models.DateTimeField(db_index=True, verbose_name='end time')), - ('time_limit', models.DurationField(blank=True, null=True, verbose_name='time limit')), - ('is_public', models.BooleanField(default=False, help_text='Should be set even for organization-private contests, where it determines whether the contest is visible to members of the specified organizations.', verbose_name='publicly visible')), - ('is_rated', models.BooleanField(default=False, help_text='Whether this contest can be rated.', verbose_name='contest rated')), - ('hide_scoreboard', models.BooleanField(default=False, help_text='Whether the scoreboard should remain hidden for the duration of the contest.', verbose_name='hide scoreboard')), - ('use_clarifications', models.BooleanField(default=True, help_text='Use clarification system instead of comments.', verbose_name='no comments')), - ('rate_all', models.BooleanField(default=False, help_text='Rate all users who joined.', verbose_name='rate all')), - ('is_private', models.BooleanField(default=False, verbose_name='private to organizations')), - ('hide_problem_tags', models.BooleanField(default=False, help_text='Whether problem tags should be hidden by default.', verbose_name='hide problem tags')), - ('run_pretests_only', models.BooleanField(default=False, help_text='Whether judges should grade pretests only, versus all testcases. Commonly set during a contest, then unset prior to rejudging user submissions when the contest ends.', verbose_name='run pretests only')), - ('og_image', models.CharField(blank=True, default='', max_length=150, verbose_name='OpenGraph image')), - ('logo_override_image', models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users inside the contest.', max_length=150, verbose_name='Logo override image')), - ('user_count', models.IntegerField(default=0, verbose_name='the amount of live participants')), - ('summary', models.TextField(blank=True, help_text='Plain-text, shown in meta description tag, e.g. for social media.', verbose_name='contest summary')), - ('access_code', models.CharField(blank=True, default='', help_text='An optional code to prompt contestants before they are allowed to join the contest. Leave it blank to disable.', max_length=255, verbose_name='access code')), - ('format_name', models.CharField(choices=[('default', 'Default')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format')), - ('format_config', jsonfield.fields.JSONField(blank=True, 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.', null=True, verbose_name='contest format configuration')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z0-9]+$", "Contest id must be ^[a-z0-9]+$" + ) + ], + verbose_name="contest id", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=100, verbose_name="contest name" + ), + ), + ( + "description", + models.TextField(blank=True, verbose_name="description"), + ), + ( + "start_time", + models.DateTimeField(db_index=True, verbose_name="start time"), + ), + ( + "end_time", + models.DateTimeField(db_index=True, verbose_name="end time"), + ), + ( + "time_limit", + models.DurationField( + blank=True, null=True, verbose_name="time limit" + ), + ), + ( + "is_public", + models.BooleanField( + default=False, + help_text="Should be set even for organization-private contests, where it determines whether the contest is visible to members of the specified organizations.", + verbose_name="publicly visible", + ), + ), + ( + "is_rated", + models.BooleanField( + default=False, + help_text="Whether this contest can be rated.", + verbose_name="contest rated", + ), + ), + ( + "hide_scoreboard", + models.BooleanField( + default=False, + help_text="Whether the scoreboard should remain hidden for the duration of the contest.", + verbose_name="hide scoreboard", + ), + ), + ( + "use_clarifications", + models.BooleanField( + default=True, + help_text="Use clarification system instead of comments.", + verbose_name="no comments", + ), + ), + ( + "rate_all", + models.BooleanField( + default=False, + help_text="Rate all users who joined.", + verbose_name="rate all", + ), + ), + ( + "is_private", + models.BooleanField( + default=False, verbose_name="private to organizations" + ), + ), + ( + "hide_problem_tags", + models.BooleanField( + default=False, + help_text="Whether problem tags should be hidden by default.", + verbose_name="hide problem tags", + ), + ), + ( + "run_pretests_only", + models.BooleanField( + default=False, + help_text="Whether judges should grade pretests only, versus all testcases. Commonly set during a contest, then unset prior to rejudging user submissions when the contest ends.", + verbose_name="run pretests only", + ), + ), + ( + "og_image", + models.CharField( + blank=True, + default="", + max_length=150, + verbose_name="OpenGraph image", + ), + ), + ( + "logo_override_image", + models.CharField( + blank=True, + default="", + help_text="This image will replace the default site logo for users inside the contest.", + max_length=150, + verbose_name="Logo override image", + ), + ), + ( + "user_count", + models.IntegerField( + default=0, verbose_name="the amount of live participants" + ), + ), + ( + "summary", + models.TextField( + blank=True, + help_text="Plain-text, shown in meta description tag, e.g. for social media.", + verbose_name="contest summary", + ), + ), + ( + "access_code", + models.CharField( + blank=True, + default="", + help_text="An optional code to prompt contestants before they are allowed to join the contest. Leave it blank to disable.", + max_length=255, + verbose_name="access code", + ), + ), + ( + "format_name", + models.CharField( + choices=[("default", "Default")], + default="default", + help_text="The contest format module to use.", + max_length=32, + verbose_name="contest format", + ), + ), + ( + "format_config", + jsonfield.fields.JSONField( + blank=True, + 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.", + null=True, + verbose_name="contest format configuration", + ), + ), ], options={ - 'verbose_name_plural': 'contests', - 'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes')), - 'verbose_name': 'contest', + "verbose_name_plural": "contests", + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ), + "verbose_name": "contest", }, ), migrations.CreateModel( - name='ContestParticipation', + name="ContestParticipation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('real_start', models.DateTimeField(db_column='start', default=django.utils.timezone.now, verbose_name='start time')), - ('score', models.IntegerField(db_index=True, default=0, verbose_name='score')), - ('cumtime', models.PositiveIntegerField(default=0, verbose_name='cumulative time')), - ('virtual', models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation', verbose_name='virtual participation id')), - ('format_data', jsonfield.fields.JSONField(blank=True, null=True, verbose_name='contest format specific data')), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to='judge.Contest', verbose_name='associated contest')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "real_start", + models.DateTimeField( + db_column="start", + default=django.utils.timezone.now, + verbose_name="start time", + ), + ), + ( + "score", + models.IntegerField(db_index=True, default=0, verbose_name="score"), + ), + ( + "cumtime", + models.PositiveIntegerField( + default=0, verbose_name="cumulative time" + ), + ), + ( + "virtual", + models.IntegerField( + default=0, + help_text="0 means non-virtual, otherwise the n-th virtual participation", + verbose_name="virtual participation id", + ), + ), + ( + "format_data", + jsonfield.fields.JSONField( + blank=True, + null=True, + verbose_name="contest format specific data", + ), + ), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to="judge.Contest", + verbose_name="associated contest", + ), + ), ], options={ - 'verbose_name_plural': 'contest participations', - 'verbose_name': 'contest participation', + "verbose_name_plural": "contest participations", + "verbose_name": "contest participation", }, ), migrations.CreateModel( - name='ContestProblem', + name="ContestProblem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('points', models.IntegerField(verbose_name='points')), - ('partial', models.BooleanField(default=True, verbose_name='partial')), - ('is_pretested', models.BooleanField(default=False, verbose_name='is pretested')), - ('order', models.PositiveIntegerField(db_index=True, verbose_name='order')), - ('output_prefix_override', models.IntegerField(blank=True, null=True, verbose_name='output prefix length override')), - ('max_submissions', models.IntegerField(default=0, help_text='Maximum number of submissions for this problem, or 0 for no limit.', validators=[django.core.validators.MinValueValidator(0, "Why include a problem you can't submit to?")])), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_problems', to='judge.Contest', verbose_name='contest')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("points", models.IntegerField(verbose_name="points")), + ("partial", models.BooleanField(default=True, verbose_name="partial")), + ( + "is_pretested", + models.BooleanField(default=False, verbose_name="is pretested"), + ), + ( + "order", + models.PositiveIntegerField(db_index=True, verbose_name="order"), + ), + ( + "output_prefix_override", + models.IntegerField( + blank=True, + null=True, + verbose_name="output prefix length override", + ), + ), + ( + "max_submissions", + models.IntegerField( + default=0, + help_text="Maximum number of submissions for this problem, or 0 for no limit.", + validators=[ + django.core.validators.MinValueValidator( + 0, "Why include a problem you can't submit to?" + ) + ], + ), + ), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contest_problems", + to="judge.Contest", + verbose_name="contest", + ), + ), ], options={ - 'verbose_name_plural': 'contest problems', - 'verbose_name': 'contest problem', + "verbose_name_plural": "contest problems", + "verbose_name": "contest problem", }, ), migrations.CreateModel( - name='ContestSubmission', + name="ContestSubmission", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('points', models.FloatField(default=0.0, verbose_name='points')), - ('is_pretest', models.BooleanField(default=False, help_text='Whether this submission was ran only on pretests.', verbose_name='is pretested')), - ('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', related_query_name='submission', to='judge.ContestParticipation', verbose_name='participation')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', related_query_name='submission', to='judge.ContestProblem', verbose_name='problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("points", models.FloatField(default=0.0, verbose_name="points")), + ( + "is_pretest", + models.BooleanField( + default=False, + help_text="Whether this submission was ran only on pretests.", + verbose_name="is pretested", + ), + ), + ( + "participation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissions", + related_query_name="submission", + to="judge.ContestParticipation", + verbose_name="participation", + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissions", + related_query_name="submission", + to="judge.ContestProblem", + verbose_name="problem", + ), + ), ], options={ - 'verbose_name_plural': 'contest submissions', - 'verbose_name': 'contest submission', + "verbose_name_plural": "contest submissions", + "verbose_name": "contest submission", }, ), migrations.CreateModel( - name='ContestTag', + name="ContestTag", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z-]+$', message='Lowercase letters and hyphens only.')], verbose_name='tag name')), - ('color', models.CharField(max_length=7, validators=[django.core.validators.RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', 'Invalid colour.')], verbose_name='tag colour')), - ('description', models.TextField(blank=True, verbose_name='tag description')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z-]+$", + message="Lowercase letters and hyphens only.", + ) + ], + verbose_name="tag name", + ), + ), + ( + "color", + models.CharField( + max_length=7, + validators=[ + django.core.validators.RegexValidator( + "^#(?:[A-Fa-f0-9]{3}){1,2}$", "Invalid colour." + ) + ], + verbose_name="tag colour", + ), + ), + ( + "description", + models.TextField(blank=True, verbose_name="tag description"), + ), ], options={ - 'verbose_name_plural': 'contest tags', - 'verbose_name': 'contest tag', + "verbose_name_plural": "contest tags", + "verbose_name": "contest tag", }, ), migrations.CreateModel( - name='Judge', + name="Judge", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='Server name, hostname-style', max_length=50, unique=True)), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='time of creation')), - ('auth_key', models.CharField(help_text='A key to authenticated this judge', max_length=100, verbose_name='authentication key')), - ('is_blocked', models.BooleanField(default=False, help_text='Whether this judge should be blocked from connecting, even if its key is correct.', verbose_name='block judge')), - ('online', models.BooleanField(default=False, verbose_name='judge online status')), - ('start_time', models.DateTimeField(null=True, verbose_name='judge start time')), - ('ping', models.FloatField(null=True, verbose_name='response time')), - ('load', models.FloatField(help_text='Load for the last minute, divided by processors to be fair.', null=True, verbose_name='system load')), - ('description', models.TextField(blank=True, verbose_name='description')), - ('last_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Last connected IP')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="Server name, hostname-style", + max_length=50, + unique=True, + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, verbose_name="time of creation" + ), + ), + ( + "auth_key", + models.CharField( + help_text="A key to authenticated this judge", + max_length=100, + verbose_name="authentication key", + ), + ), + ( + "is_blocked", + models.BooleanField( + default=False, + help_text="Whether this judge should be blocked from connecting, even if its key is correct.", + verbose_name="block judge", + ), + ), + ( + "online", + models.BooleanField( + default=False, verbose_name="judge online status" + ), + ), + ( + "start_time", + models.DateTimeField(null=True, verbose_name="judge start time"), + ), + ("ping", models.FloatField(null=True, verbose_name="response time")), + ( + "load", + models.FloatField( + help_text="Load for the last minute, divided by processors to be fair.", + null=True, + verbose_name="system load", + ), + ), + ( + "description", + models.TextField(blank=True, verbose_name="description"), + ), + ( + "last_ip", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="Last connected IP" + ), + ), ], options={ - 'ordering': ['name'], - 'verbose_name_plural': 'judges', - 'verbose_name': 'judge', + "ordering": ["name"], + "verbose_name_plural": "judges", + "verbose_name": "judge", }, ), migrations.CreateModel( - name='Language', + name="Language", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(help_text='The identifier for this language; the same as its executor id for judges.', max_length=6, unique=True, verbose_name='short identifier')), - ('name', models.CharField(help_text='Longer name for the language, e.g. "Python 2" or "C++11".', max_length=20, verbose_name='long name')), - ('short_name', models.CharField(blank=True, help_text='More readable, but short, name to display publicly; e.g. "PY2" or "C++11". If left blank, it will default to the short identifier.', max_length=10, null=True, verbose_name='short name')), - ('common_name', models.CharField(help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++"', max_length=10, verbose_name='common name')), - ('ace', models.CharField(help_text='Language ID for Ace.js editor highlighting, appended to "mode-" to determine the Ace JavaScript file to use, e.g., "python".', max_length=20, verbose_name='ace mode name')), - ('pygments', models.CharField(help_text='Language ID for Pygments highlighting in source windows.', max_length=20, verbose_name='pygments name')), - ('template', models.TextField(blank=True, help_text='Code template to display in submission editor.', verbose_name='code template')), - ('info', models.CharField(blank=True, help_text="Do not set this unless you know what you're doing! It will override the usually more specific, judge-provided runtime info!", max_length=50, verbose_name='runtime info override')), - ('description', models.TextField(blank=True, help_text='Use field this to inform users of quirks with your environment, additional restrictions, etc.', verbose_name='language description')), - ('extension', models.CharField(help_text='The extension of source files, e.g., "py" or "cpp".', max_length=10, verbose_name='extension')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + help_text="The identifier for this language; the same as its executor id for judges.", + max_length=6, + unique=True, + verbose_name="short identifier", + ), + ), + ( + "name", + models.CharField( + help_text='Longer name for the language, e.g. "Python 2" or "C++11".', + max_length=20, + verbose_name="long name", + ), + ), + ( + "short_name", + models.CharField( + blank=True, + help_text='More readable, but short, name to display publicly; e.g. "PY2" or "C++11". If left blank, it will default to the short identifier.', + max_length=10, + null=True, + verbose_name="short name", + ), + ), + ( + "common_name", + models.CharField( + help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++"', + max_length=10, + verbose_name="common name", + ), + ), + ( + "ace", + models.CharField( + help_text='Language ID for Ace.js editor highlighting, appended to "mode-" to determine the Ace JavaScript file to use, e.g., "python".', + max_length=20, + verbose_name="ace mode name", + ), + ), + ( + "pygments", + models.CharField( + help_text="Language ID for Pygments highlighting in source windows.", + max_length=20, + verbose_name="pygments name", + ), + ), + ( + "template", + models.TextField( + blank=True, + help_text="Code template to display in submission editor.", + verbose_name="code template", + ), + ), + ( + "info", + models.CharField( + blank=True, + help_text="Do not set this unless you know what you're doing! It will override the usually more specific, judge-provided runtime info!", + max_length=50, + verbose_name="runtime info override", + ), + ), + ( + "description", + models.TextField( + blank=True, + help_text="Use field this to inform users of quirks with your environment, additional restrictions, etc.", + verbose_name="language description", + ), + ), + ( + "extension", + models.CharField( + help_text='The extension of source files, e.g., "py" or "cpp".', + max_length=10, + verbose_name="extension", + ), + ), ], options={ - 'ordering': ['key'], - 'verbose_name_plural': 'languages', - 'verbose_name': 'language', + "ordering": ["key"], + "verbose_name_plural": "languages", + "verbose_name": "language", }, ), migrations.CreateModel( - name='LanguageLimit', + name="LanguageLimit", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time_limit', models.FloatField(verbose_name='time limit')), - ('memory_limit', models.IntegerField(verbose_name='memory limit')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='language')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time_limit", models.FloatField(verbose_name="time limit")), + ("memory_limit", models.IntegerField(verbose_name="memory limit")), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="language", + ), + ), ], options={ - 'verbose_name_plural': 'language-specific resource limits', - 'verbose_name': 'language-specific resource limit', + "verbose_name_plural": "language-specific resource limits", + "verbose_name": "language-specific resource limit", }, ), migrations.CreateModel( - name='License', + name="License", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[-\\w.]+$', 'License key must be ^[-\\w.]+$')], verbose_name='key')), - ('link', models.CharField(max_length=256, verbose_name='link')), - ('name', models.CharField(max_length=256, verbose_name='full name')), - ('display', models.CharField(blank=True, help_text='Displayed on pages under this license', max_length=256, verbose_name='short name')), - ('icon', models.CharField(blank=True, help_text='URL to the icon', max_length=256, verbose_name='icon')), - ('text', models.TextField(verbose_name='license text')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[-\\w.]+$", "License key must be ^[-\\w.]+$" + ) + ], + verbose_name="key", + ), + ), + ("link", models.CharField(max_length=256, verbose_name="link")), + ("name", models.CharField(max_length=256, verbose_name="full name")), + ( + "display", + models.CharField( + blank=True, + help_text="Displayed on pages under this license", + max_length=256, + verbose_name="short name", + ), + ), + ( + "icon", + models.CharField( + blank=True, + help_text="URL to the icon", + max_length=256, + verbose_name="icon", + ), + ), + ("text", models.TextField(verbose_name="license text")), ], options={ - 'verbose_name_plural': 'licenses', - 'verbose_name': 'license', + "verbose_name_plural": "licenses", + "verbose_name": "license", }, ), migrations.CreateModel( - name='MiscConfig', + name="MiscConfig", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(db_index=True, max_length=30)), - ('value', models.TextField(blank=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("key", models.CharField(db_index=True, max_length=30)), + ("value", models.TextField(blank=True)), ], options={ - 'verbose_name_plural': 'miscellaneous configuration', - 'verbose_name': 'configuration item', + "verbose_name_plural": "miscellaneous configuration", + "verbose_name": "configuration item", }, ), migrations.CreateModel( - name='NavigationBar', + name="NavigationBar", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.PositiveIntegerField(db_index=True, verbose_name='order')), - ('key', models.CharField(max_length=10, unique=True, verbose_name='identifier')), - ('label', models.CharField(max_length=20, verbose_name='label')), - ('path', models.CharField(max_length=255, verbose_name='link path')), - ('regex', models.TextField(validators=[judge.models.interface.validate_regex], verbose_name='highlight regex')), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='judge.NavigationBar', verbose_name='parent item')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "order", + models.PositiveIntegerField(db_index=True, verbose_name="order"), + ), + ( + "key", + models.CharField( + max_length=10, unique=True, verbose_name="identifier" + ), + ), + ("label", models.CharField(max_length=20, verbose_name="label")), + ("path", models.CharField(max_length=255, verbose_name="link path")), + ( + "regex", + models.TextField( + validators=[judge.models.interface.validate_regex], + verbose_name="highlight regex", + ), + ), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "parent", + mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="judge.NavigationBar", + verbose_name="parent item", + ), + ), ], options={ - 'verbose_name_plural': 'navigation bar', - 'verbose_name': 'navigation item', + "verbose_name_plural": "navigation bar", + "verbose_name": "navigation item", }, ), migrations.CreateModel( - name='Organization', + name="Organization", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='organization title')), - ('slug', models.SlugField(help_text='Organization name shown in URL', max_length=128, verbose_name='organization slug')), - ('short_name', models.CharField(help_text='Displayed beside user name during contests', max_length=20, verbose_name='short name')), - ('about', models.TextField(verbose_name='organization description')), - ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), - ('is_open', models.BooleanField(default=True, help_text='Allow joining organization', verbose_name='is open organization?')), - ('slots', models.IntegerField(blank=True, help_text='Maximum amount of users in this organization, only applicable to private organizations', null=True, verbose_name='maximum size')), - ('access_code', models.CharField(blank=True, help_text='Student access code', max_length=7, null=True, verbose_name='access code')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=128, verbose_name="organization title"), + ), + ( + "slug", + models.SlugField( + help_text="Organization name shown in URL", + max_length=128, + verbose_name="organization slug", + ), + ), + ( + "short_name", + models.CharField( + help_text="Displayed beside user name during contests", + max_length=20, + verbose_name="short name", + ), + ), + ("about", models.TextField(verbose_name="organization description")), + ( + "creation_date", + models.DateTimeField( + auto_now_add=True, verbose_name="creation date" + ), + ), + ( + "is_open", + models.BooleanField( + default=True, + help_text="Allow joining organization", + verbose_name="is open organization?", + ), + ), + ( + "slots", + models.IntegerField( + blank=True, + help_text="Maximum amount of users in this organization, only applicable to private organizations", + null=True, + verbose_name="maximum size", + ), + ), + ( + "access_code", + models.CharField( + blank=True, + help_text="Student access code", + max_length=7, + null=True, + verbose_name="access code", + ), + ), ], options={ - 'ordering': ['name'], - 'verbose_name_plural': 'organizations', - 'permissions': (('organization_admin', 'Administer organizations'), ('edit_all_organization', 'Edit all organizations')), - 'verbose_name': 'organization', + "ordering": ["name"], + "verbose_name_plural": "organizations", + "permissions": ( + ("organization_admin", "Administer organizations"), + ("edit_all_organization", "Edit all organizations"), + ), + "verbose_name": "organization", }, ), migrations.CreateModel( - name='OrganizationRequest', + name="OrganizationRequest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='request time')), - ('state', models.CharField(choices=[('P', 'Pending'), ('A', 'Approved'), ('R', 'Rejected')], max_length=1, verbose_name='state')), - ('reason', models.TextField(verbose_name='reason')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='judge.Organization', verbose_name='organization')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField( + auto_now_add=True, verbose_name="request time" + ), + ), + ( + "state", + models.CharField( + choices=[ + ("P", "Pending"), + ("A", "Approved"), + ("R", "Rejected"), + ], + max_length=1, + verbose_name="state", + ), + ), + ("reason", models.TextField(verbose_name="reason")), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="requests", + to="judge.Organization", + verbose_name="organization", + ), + ), ], options={ - 'verbose_name_plural': 'organization join requests', - 'verbose_name': 'organization join request', + "verbose_name_plural": "organization join requests", + "verbose_name": "organization join request", }, ), migrations.CreateModel( - name='PrivateMessage', + name="PrivateMessage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='message title')), - ('content', models.TextField(verbose_name='message body')), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='message timestamp')), - ('read', models.BooleanField(default=False, verbose_name='read')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField(max_length=50, verbose_name="message title"), + ), + ("content", models.TextField(verbose_name="message body")), + ( + "timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="message timestamp" + ), + ), + ("read", models.BooleanField(default=False, verbose_name="read")), ], ), migrations.CreateModel( - name='PrivateMessageThread', + name="PrivateMessageThread", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('messages', models.ManyToManyField(to='judge.PrivateMessage', verbose_name='messages in the thread')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "messages", + models.ManyToManyField( + to="judge.PrivateMessage", verbose_name="messages in the thread" + ), + ), ], ), migrations.CreateModel( - name='Problem', + name="Problem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Problem code must be ^[a-z0-9]+$')], verbose_name='problem code')), - ('name', models.CharField(db_index=True, max_length=100, verbose_name='problem name')), - ('description', models.TextField(verbose_name='problem body')), - ('time_limit', models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', verbose_name='time limit')), - ('memory_limit', models.IntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit')), - ('short_circuit', models.BooleanField(default=False)), - ('points', models.FloatField(verbose_name='points')), - ('partial', models.BooleanField(default=False, verbose_name='allows partial points')), - ('is_public', models.BooleanField(db_index=True, default=False, verbose_name='publicly visible')), - ('is_manually_managed', models.BooleanField(db_index=True, default=False, help_text='Whether judges should be allowed to manage data or not', verbose_name='manually managed')), - ('date', models.DateTimeField(blank=True, db_index=True, help_text="Doesn't have magic ability to auto-publish due to backward compatibility", null=True, verbose_name='date of publishing')), - ('og_image', models.CharField(blank=True, max_length=150, verbose_name='OpenGraph image')), - ('summary', models.TextField(blank=True, help_text='Plain-text, shown in meta description tag, e.g. for social media.', verbose_name='problem summary')), - ('user_count', models.IntegerField(default=0, help_text='The number of users who solved the problem.', verbose_name='number of users')), - ('ac_rate', models.FloatField(default=0, verbose_name='solve rate')), - ('is_organization_private', models.BooleanField(default=False, verbose_name='private to organizations')), - ('allowed_languages', models.ManyToManyField(to='judge.Language', verbose_name='allowed languages')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z0-9]+$", "Problem code must be ^[a-z0-9]+$" + ) + ], + verbose_name="problem code", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=100, verbose_name="problem name" + ), + ), + ("description", models.TextField(verbose_name="problem body")), + ( + "time_limit", + models.FloatField( + help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.", + verbose_name="time limit", + ), + ), + ( + "memory_limit", + models.IntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).", + verbose_name="memory limit", + ), + ), + ("short_circuit", models.BooleanField(default=False)), + ("points", models.FloatField(verbose_name="points")), + ( + "partial", + models.BooleanField( + default=False, verbose_name="allows partial points" + ), + ), + ( + "is_public", + models.BooleanField( + db_index=True, default=False, verbose_name="publicly visible" + ), + ), + ( + "is_manually_managed", + models.BooleanField( + db_index=True, + default=False, + help_text="Whether judges should be allowed to manage data or not", + verbose_name="manually managed", + ), + ), + ( + "date", + models.DateTimeField( + blank=True, + db_index=True, + help_text="Doesn't have magic ability to auto-publish due to backward compatibility", + null=True, + verbose_name="date of publishing", + ), + ), + ( + "og_image", + models.CharField( + blank=True, max_length=150, verbose_name="OpenGraph image" + ), + ), + ( + "summary", + models.TextField( + blank=True, + help_text="Plain-text, shown in meta description tag, e.g. for social media.", + verbose_name="problem summary", + ), + ), + ( + "user_count", + models.IntegerField( + default=0, + help_text="The number of users who solved the problem.", + verbose_name="number of users", + ), + ), + ("ac_rate", models.FloatField(default=0, verbose_name="solve rate")), + ( + "is_organization_private", + models.BooleanField( + default=False, verbose_name="private to organizations" + ), + ), + ( + "allowed_languages", + models.ManyToManyField( + to="judge.Language", verbose_name="allowed languages" + ), + ), ], options={ - 'verbose_name_plural': 'problems', - 'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems')), - 'verbose_name': 'problem', + "verbose_name_plural": "problems", + "permissions": ( + ("see_private_problem", "See hidden problems"), + ("edit_own_problem", "Edit own problems"), + ("edit_all_problem", "Edit all problems"), + ("edit_public_problem", "Edit all public problems"), + ("clone_problem", "Clone problem"), + ("change_public_visibility", "Change is_public field"), + ("change_manually_managed", "Change is_manually_managed field"), + ("see_organization_problem", "See organization-private problems"), + ), + "verbose_name": "problem", }, ), migrations.CreateModel( - name='ProblemClarification', + name="ProblemClarification", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField(verbose_name='clarification body')), - ('date', models.DateTimeField(auto_now_add=True, verbose_name='clarification timestamp')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem', verbose_name='clarified problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.TextField(verbose_name="clarification body")), + ( + "date", + models.DateTimeField( + auto_now_add=True, verbose_name="clarification timestamp" + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Problem", + verbose_name="clarified problem", + ), + ), ], ), migrations.CreateModel( - name='ProblemData', + name="ProblemData", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('zipfile', models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='data zip file')), - ('generator', models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='generator file')), - ('output_prefix', models.IntegerField(blank=True, null=True, verbose_name='output prefix length')), - ('output_limit', models.IntegerField(blank=True, null=True, verbose_name='output limit length')), - ('feedback', models.TextField(blank=True, verbose_name='init.yml generation feedback')), - ('checker', models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker')), - ('checker_args', models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments')), - ('problem', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='data_files', to='judge.Problem', verbose_name='problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "zipfile", + models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + verbose_name="data zip file", + ), + ), + ( + "generator", + models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + verbose_name="generator file", + ), + ), + ( + "output_prefix", + models.IntegerField( + blank=True, null=True, verbose_name="output prefix length" + ), + ), + ( + "output_limit", + models.IntegerField( + blank=True, null=True, verbose_name="output limit length" + ), + ), + ( + "feedback", + models.TextField( + blank=True, verbose_name="init.yml generation feedback" + ), + ), + ( + "checker", + models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ], + max_length=10, + verbose_name="checker", + ), + ), + ( + "checker_args", + models.TextField( + blank=True, + help_text="checker arguments as a JSON object", + verbose_name="checker arguments", + ), + ), + ( + "problem", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="data_files", + to="judge.Problem", + verbose_name="problem", + ), + ), ], ), migrations.CreateModel( - name='ProblemGroup', + name="ProblemGroup", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, verbose_name='problem group ID')), - ('full_name', models.CharField(max_length=100, verbose_name='problem group name')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, unique=True, verbose_name="problem group ID" + ), + ), + ( + "full_name", + models.CharField(max_length=100, verbose_name="problem group name"), + ), ], options={ - 'ordering': ['full_name'], - 'verbose_name_plural': 'problem groups', - 'verbose_name': 'problem group', + "ordering": ["full_name"], + "verbose_name_plural": "problem groups", + "verbose_name": "problem group", }, ), migrations.CreateModel( - name='ProblemTestCase', + name="ProblemTestCase", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.IntegerField(verbose_name='case position')), - ('type', models.CharField(choices=[('C', 'Normal case'), ('S', 'Batch start'), ('E', 'Batch end')], default='C', max_length=1, verbose_name='case type')), - ('input_file', models.CharField(blank=True, max_length=100, verbose_name='input file name')), - ('output_file', models.CharField(blank=True, max_length=100, verbose_name='output file name')), - ('generator_args', models.TextField(blank=True, verbose_name='generator arguments')), - ('points', models.IntegerField(blank=True, null=True, verbose_name='point value')), - ('is_pretest', models.BooleanField(verbose_name='case is pretest?')), - ('output_prefix', models.IntegerField(blank=True, null=True, verbose_name='output prefix length')), - ('output_limit', models.IntegerField(blank=True, null=True, verbose_name='output limit length')), - ('checker', models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker')), - ('checker_args', models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments')), - ('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cases', to='judge.Problem', verbose_name='problem data set')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.IntegerField(verbose_name="case position")), + ( + "type", + models.CharField( + choices=[ + ("C", "Normal case"), + ("S", "Batch start"), + ("E", "Batch end"), + ], + default="C", + max_length=1, + verbose_name="case type", + ), + ), + ( + "input_file", + models.CharField( + blank=True, max_length=100, verbose_name="input file name" + ), + ), + ( + "output_file", + models.CharField( + blank=True, max_length=100, verbose_name="output file name" + ), + ), + ( + "generator_args", + models.TextField(blank=True, verbose_name="generator arguments"), + ), + ( + "points", + models.IntegerField( + blank=True, null=True, verbose_name="point value" + ), + ), + ("is_pretest", models.BooleanField(verbose_name="case is pretest?")), + ( + "output_prefix", + models.IntegerField( + blank=True, null=True, verbose_name="output prefix length" + ), + ), + ( + "output_limit", + models.IntegerField( + blank=True, null=True, verbose_name="output limit length" + ), + ), + ( + "checker", + models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ], + max_length=10, + verbose_name="checker", + ), + ), + ( + "checker_args", + models.TextField( + blank=True, + help_text="checker arguments as a JSON object", + verbose_name="checker arguments", + ), + ), + ( + "dataset", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cases", + to="judge.Problem", + verbose_name="problem data set", + ), + ), ], ), migrations.CreateModel( - name='ProblemTranslation', + name="ProblemTranslation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language', models.CharField(choices=[('de', 'German'), ('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ko', 'Korean'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sr-latn', 'Serbian (Latin)'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese')], max_length=7, verbose_name='language')), - ('name', models.CharField(db_index=True, max_length=100, verbose_name='translated name')), - ('description', models.TextField(verbose_name='translated description')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='judge.Problem', verbose_name='problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language", + models.CharField( + choices=[ + ("de", "German"), + ("en", "English"), + ("es", "Spanish"), + ("fr", "French"), + ("hr", "Croatian"), + ("hu", "Hungarian"), + ("ko", "Korean"), + ("ro", "Romanian"), + ("ru", "Russian"), + ("sr-latn", "Serbian (Latin)"), + ("tr", "Turkish"), + ("vi", "Vietnamese"), + ("zh-hans", "Simplified Chinese"), + ], + max_length=7, + verbose_name="language", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=100, verbose_name="translated name" + ), + ), + ( + "description", + models.TextField(verbose_name="translated description"), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="translations", + to="judge.Problem", + verbose_name="problem", + ), + ), ], options={ - 'verbose_name_plural': 'problem translations', - 'verbose_name': 'problem translation', + "verbose_name_plural": "problem translations", + "verbose_name": "problem translation", }, ), migrations.CreateModel( - name='ProblemType', + name="ProblemType", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20, unique=True, verbose_name='problem category ID')), - ('full_name', models.CharField(max_length=100, verbose_name='problem category name')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, unique=True, verbose_name="problem category ID" + ), + ), + ( + "full_name", + models.CharField( + max_length=100, verbose_name="problem category name" + ), + ), ], options={ - 'ordering': ['full_name'], - 'verbose_name_plural': 'problem types', - 'verbose_name': 'problem type', + "ordering": ["full_name"], + "verbose_name_plural": "problem types", + "verbose_name": "problem type", }, ), migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('about', models.TextField(blank=True, null=True, verbose_name='self-description')), - ('timezone', models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Toronto', max_length=50, verbose_name='location')), - ('points', models.FloatField(db_index=True, default=0)), - ('performance_points', models.FloatField(db_index=True, default=0)), - ('problem_count', models.IntegerField(db_index=True, default=0)), - ('ace_theme', models.CharField(choices=[('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='github', max_length=30)), - ('last_access', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last access time')), - ('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='last IP')), - ('display_rank', models.CharField(choices=[('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin')], default='user', max_length=10, verbose_name='display rank')), - ('mute', models.BooleanField(default=False, help_text='Some users are at their best when silent.', verbose_name='comment mute')), - ('is_unlisted', models.BooleanField(default=False, help_text='User will not be ranked.', verbose_name='unlisted user')), - ('rating', models.IntegerField(default=None, null=True)), - ('user_script', models.TextField(blank=True, default='', help_text='User-defined JavaScript for site customization.', max_length=65536, verbose_name='user script')), - ('math_engine', models.CharField(choices=[('tex', 'Leave as LaTeX'), ('svg', 'SVG with PNG fallback'), ('mml', 'MathML only'), ('jax', 'MathJax with SVG/PNG fallback'), ('auto', 'Detect best quality')], default='auto', help_text='the rendering engine used to render math', max_length=4, verbose_name='math engine')), - ('is_totp_enabled', models.BooleanField(default=False, help_text='check to enable TOTP-based two factor authentication', verbose_name='2FA enabled')), - ('totp_key', judge.models.profile.EncryptedNullCharField(blank=True, help_text='32 character base32-encoded key for TOTP', max_length=32, null=True, validators=[django.core.validators.RegexValidator('^$|^[A-Z2-7]{32}$', 'TOTP key must be empty or base32')], verbose_name='TOTP key')), - ('notes', models.TextField(blank=True, help_text='Notes for administrators regarding this user.', null=True, verbose_name='internal notes')), - ('current_contest', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.ContestParticipation', verbose_name='current contest')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='preferred language')), - ('organizations', sortedm2m.fields.SortedManyToManyField(blank=True, help_text=None, related_name='members', related_query_name='member', to='judge.Organization', verbose_name='organization')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user associated')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "about", + models.TextField( + blank=True, null=True, verbose_name="self-description" + ), + ), + ( + "timezone", + models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ( + "America/Indiana/Indianapolis", + "Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ( + "America/Kentucky/Louisville", + "Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "Kentucky/Monticello", + ), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ( + "America/North_Dakota/Beulah", + "North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="America/Toronto", + max_length=50, + verbose_name="location", + ), + ), + ("points", models.FloatField(db_index=True, default=0)), + ("performance_points", models.FloatField(db_index=True, default=0)), + ("problem_count", models.IntegerField(db_index=True, default=0)), + ( + "ace_theme", + models.CharField( + choices=[ + ("ambiance", "Ambiance"), + ("chaos", "Chaos"), + ("chrome", "Chrome"), + ("clouds", "Clouds"), + ("clouds_midnight", "Clouds Midnight"), + ("cobalt", "Cobalt"), + ("crimson_editor", "Crimson Editor"), + ("dawn", "Dawn"), + ("dreamweaver", "Dreamweaver"), + ("eclipse", "Eclipse"), + ("github", "Github"), + ("idle_fingers", "Idle Fingers"), + ("katzenmilch", "Katzenmilch"), + ("kr_theme", "KR Theme"), + ("kuroir", "Kuroir"), + ("merbivore", "Merbivore"), + ("merbivore_soft", "Merbivore Soft"), + ("mono_industrial", "Mono Industrial"), + ("monokai", "Monokai"), + ("pastel_on_dark", "Pastel on Dark"), + ("solarized_dark", "Solarized Dark"), + ("solarized_light", "Solarized Light"), + ("terminal", "Terminal"), + ("textmate", "Textmate"), + ("tomorrow", "Tomorrow"), + ("tomorrow_night", "Tomorrow Night"), + ("tomorrow_night_blue", "Tomorrow Night Blue"), + ("tomorrow_night_bright", "Tomorrow Night Bright"), + ("tomorrow_night_eighties", "Tomorrow Night Eighties"), + ("twilight", "Twilight"), + ("vibrant_ink", "Vibrant Ink"), + ("xcode", "XCode"), + ], + default="github", + max_length=30, + ), + ), + ( + "last_access", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="last access time", + ), + ), + ( + "ip", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="last IP" + ), + ), + ( + "display_rank", + models.CharField( + choices=[ + ("user", "Normal User"), + ("setter", "Problem Setter"), + ("admin", "Admin"), + ], + default="user", + max_length=10, + verbose_name="display rank", + ), + ), + ( + "mute", + models.BooleanField( + default=False, + help_text="Some users are at their best when silent.", + verbose_name="comment mute", + ), + ), + ( + "is_unlisted", + models.BooleanField( + default=False, + help_text="User will not be ranked.", + verbose_name="unlisted user", + ), + ), + ("rating", models.IntegerField(default=None, null=True)), + ( + "user_script", + models.TextField( + blank=True, + default="", + help_text="User-defined JavaScript for site customization.", + max_length=65536, + verbose_name="user script", + ), + ), + ( + "math_engine", + models.CharField( + choices=[ + ("tex", "Leave as LaTeX"), + ("svg", "SVG with PNG fallback"), + ("mml", "MathML only"), + ("jax", "MathJax with SVG/PNG fallback"), + ("auto", "Detect best quality"), + ], + default="auto", + help_text="the rendering engine used to render math", + max_length=4, + verbose_name="math engine", + ), + ), + ( + "is_totp_enabled", + models.BooleanField( + default=False, + help_text="check to enable TOTP-based two factor authentication", + verbose_name="2FA enabled", + ), + ), + ( + "totp_key", + judge.models.profile.EncryptedNullCharField( + blank=True, + help_text="32 character base32-encoded key for TOTP", + max_length=32, + null=True, + validators=[ + django.core.validators.RegexValidator( + "^$|^[A-Z2-7]{32}$", "TOTP key must be empty or base32" + ) + ], + verbose_name="TOTP key", + ), + ), + ( + "notes", + models.TextField( + blank=True, + help_text="Notes for administrators regarding this user.", + null=True, + verbose_name="internal notes", + ), + ), + ( + "current_contest", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="judge.ContestParticipation", + verbose_name="current contest", + ), + ), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="preferred language", + ), + ), + ( + "organizations", + sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text=None, + related_name="members", + related_query_name="member", + to="judge.Organization", + verbose_name="organization", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="user associated", + ), + ), ], options={ - 'verbose_name_plural': 'user profiles', - 'permissions': (('test_site', 'Shows in-progress development stuff'), ('totp', 'Edit TOTP settings')), - 'verbose_name': 'user profile', + "verbose_name_plural": "user profiles", + "permissions": ( + ("test_site", "Shows in-progress development stuff"), + ("totp", "Edit TOTP settings"), + ), + "verbose_name": "user profile", }, ), migrations.CreateModel( - name='Rating', + name="Rating", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rank', models.IntegerField(verbose_name='rank')), - ('rating', models.IntegerField(verbose_name='rating')), - ('volatility', models.IntegerField(verbose_name='volatility')), - ('last_rated', models.DateTimeField(db_index=True, verbose_name='last rated')), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='judge.Contest', verbose_name='contest')), - ('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='rating', to='judge.ContestParticipation', verbose_name='participation')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='judge.Profile', verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("rank", models.IntegerField(verbose_name="rank")), + ("rating", models.IntegerField(verbose_name="rating")), + ("volatility", models.IntegerField(verbose_name="volatility")), + ( + "last_rated", + models.DateTimeField(db_index=True, verbose_name="last rated"), + ), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ratings", + to="judge.Contest", + verbose_name="contest", + ), + ), + ( + "participation", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="rating", + to="judge.ContestParticipation", + verbose_name="participation", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ratings", + to="judge.Profile", + verbose_name="user", + ), + ), ], options={ - 'verbose_name_plural': 'contest ratings', - 'verbose_name': 'contest rating', + "verbose_name_plural": "contest ratings", + "verbose_name": "contest rating", }, ), migrations.CreateModel( - name='RuntimeVersion', + name="RuntimeVersion", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=64, verbose_name='runtime name')), - ('version', models.CharField(blank=True, max_length=64, verbose_name='runtime version')), - ('priority', models.IntegerField(default=0, verbose_name='order in which to display this runtime')), - ('judge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Judge', verbose_name='judge on which this runtime exists')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='language to which this runtime belongs')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=64, verbose_name="runtime name")), + ( + "version", + models.CharField( + blank=True, max_length=64, verbose_name="runtime version" + ), + ), + ( + "priority", + models.IntegerField( + default=0, verbose_name="order in which to display this runtime" + ), + ), + ( + "judge", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Judge", + verbose_name="judge on which this runtime exists", + ), + ), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="language to which this runtime belongs", + ), + ), ], ), migrations.CreateModel( - name='Solution', + name="Solution", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_public', models.BooleanField(default=False, verbose_name='public visibility')), - ('publish_on', models.DateTimeField(verbose_name='publish date')), - ('content', models.TextField(verbose_name='editorial content')), - ('authors', models.ManyToManyField(blank=True, to='judge.Profile', verbose_name='authors')), - ('problem', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='solution', to='judge.Problem', verbose_name='associated problem')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "is_public", + models.BooleanField( + default=False, verbose_name="public visibility" + ), + ), + ("publish_on", models.DateTimeField(verbose_name="publish date")), + ("content", models.TextField(verbose_name="editorial content")), + ( + "authors", + models.ManyToManyField( + blank=True, to="judge.Profile", verbose_name="authors" + ), + ), + ( + "problem", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="solution", + to="judge.Problem", + verbose_name="associated problem", + ), + ), ], options={ - 'verbose_name_plural': 'solutions', - 'permissions': (('see_private_solution', 'See hidden solutions'),), - 'verbose_name': 'solution', + "verbose_name_plural": "solutions", + "permissions": (("see_private_solution", "See hidden solutions"),), + "verbose_name": "solution", }, ), migrations.CreateModel( - name='Submission', + name="Submission", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='submission time')), - ('time', models.FloatField(db_index=True, null=True, verbose_name='execution time')), - ('memory', models.FloatField(null=True, verbose_name='memory usage')), - ('points', models.FloatField(db_index=True, null=True, verbose_name='points granted')), - ('source', models.TextField(max_length=65536, verbose_name='source code')), - ('status', models.CharField(choices=[('QU', 'Queued'), ('P', 'Processing'), ('G', 'Grading'), ('D', 'Completed'), ('IE', 'Internal Error'), ('CE', 'Compile Error'), ('AB', 'Aborted')], db_index=True, default='QU', max_length=2, verbose_name='status')), - ('result', models.CharField(blank=True, choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short circuit'), ('AB', 'Aborted')], db_index=True, default=None, max_length=3, null=True, verbose_name='result')), - ('error', models.TextField(blank=True, null=True, verbose_name='compile errors')), - ('current_testcase', models.IntegerField(default=0)), - ('batch', models.BooleanField(default=False, verbose_name='batched cases')), - ('case_points', models.FloatField(default=0, verbose_name='test case points')), - ('case_total', models.FloatField(default=0, verbose_name='test case total points')), - ('was_rejudged', models.BooleanField(default=False, verbose_name='was rejudged by admin')), - ('is_pretested', models.BooleanField(default=False, verbose_name='was ran on pretests only')), - ('judged_on', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.Judge', verbose_name='judged on')), - ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='submission language')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "date", + models.DateTimeField( + auto_now_add=True, db_index=True, verbose_name="submission time" + ), + ), + ( + "time", + models.FloatField( + db_index=True, null=True, verbose_name="execution time" + ), + ), + ("memory", models.FloatField(null=True, verbose_name="memory usage")), + ( + "points", + models.FloatField( + db_index=True, null=True, verbose_name="points granted" + ), + ), + ( + "source", + models.TextField(max_length=65536, verbose_name="source code"), + ), + ( + "status", + models.CharField( + choices=[ + ("QU", "Queued"), + ("P", "Processing"), + ("G", "Grading"), + ("D", "Completed"), + ("IE", "Internal Error"), + ("CE", "Compile Error"), + ("AB", "Aborted"), + ], + db_index=True, + default="QU", + max_length=2, + verbose_name="status", + ), + ), + ( + "result", + models.CharField( + blank=True, + choices=[ + ("AC", "Accepted"), + ("WA", "Wrong Answer"), + ("TLE", "Time Limit Exceeded"), + ("MLE", "Memory Limit Exceeded"), + ("OLE", "Output Limit Exceeded"), + ("IR", "Invalid Return"), + ("RTE", "Runtime Error"), + ("CE", "Compile Error"), + ("IE", "Internal Error"), + ("SC", "Short circuit"), + ("AB", "Aborted"), + ], + db_index=True, + default=None, + max_length=3, + null=True, + verbose_name="result", + ), + ), + ( + "error", + models.TextField( + blank=True, null=True, verbose_name="compile errors" + ), + ), + ("current_testcase", models.IntegerField(default=0)), + ( + "batch", + models.BooleanField(default=False, verbose_name="batched cases"), + ), + ( + "case_points", + models.FloatField(default=0, verbose_name="test case points"), + ), + ( + "case_total", + models.FloatField(default=0, verbose_name="test case total points"), + ), + ( + "was_rejudged", + models.BooleanField( + default=False, verbose_name="was rejudged by admin" + ), + ), + ( + "is_pretested", + models.BooleanField( + default=False, verbose_name="was ran on pretests only" + ), + ), + ( + "judged_on", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="judge.Judge", + verbose_name="judged on", + ), + ), + ( + "language", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Language", + verbose_name="submission language", + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="judge.Problem" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="judge.Profile" + ), + ), ], options={ - 'verbose_name_plural': 'submissions', - 'permissions': (('abort_any_submission', 'Abort any submission'), ('rejudge_submission', 'Rejudge the submission'), ('rejudge_submission_lot', 'Rejudge a lot of submissions'), ('spam_submission', 'Submit without limit'), ('view_all_submission', 'View all submission'), ('resubmit_other', "Resubmit others' submission")), - 'verbose_name': 'submission', + "verbose_name_plural": "submissions", + "permissions": ( + ("abort_any_submission", "Abort any submission"), + ("rejudge_submission", "Rejudge the submission"), + ("rejudge_submission_lot", "Rejudge a lot of submissions"), + ("spam_submission", "Submit without limit"), + ("view_all_submission", "View all submission"), + ("resubmit_other", "Resubmit others' submission"), + ), + "verbose_name": "submission", }, ), migrations.CreateModel( - name='SubmissionTestCase', + name="SubmissionTestCase", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('case', models.IntegerField(verbose_name='test case ID')), - ('status', models.CharField(choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short circuit'), ('AB', 'Aborted')], max_length=3, verbose_name='status flag')), - ('time', models.FloatField(null=True, verbose_name='execution time')), - ('memory', models.FloatField(null=True, verbose_name='memory usage')), - ('points', models.FloatField(null=True, verbose_name='points granted')), - ('total', models.FloatField(null=True, verbose_name='points possible')), - ('batch', models.IntegerField(null=True, verbose_name='batch number')), - ('feedback', models.CharField(blank=True, max_length=50, verbose_name='judging feedback')), - ('extended_feedback', models.TextField(blank=True, verbose_name='extended judging feedback')), - ('output', models.TextField(blank=True, verbose_name='program output')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_cases', to='judge.Submission', verbose_name='associated submission')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("case", models.IntegerField(verbose_name="test case ID")), + ( + "status", + models.CharField( + choices=[ + ("AC", "Accepted"), + ("WA", "Wrong Answer"), + ("TLE", "Time Limit Exceeded"), + ("MLE", "Memory Limit Exceeded"), + ("OLE", "Output Limit Exceeded"), + ("IR", "Invalid Return"), + ("RTE", "Runtime Error"), + ("CE", "Compile Error"), + ("IE", "Internal Error"), + ("SC", "Short circuit"), + ("AB", "Aborted"), + ], + max_length=3, + verbose_name="status flag", + ), + ), + ("time", models.FloatField(null=True, verbose_name="execution time")), + ("memory", models.FloatField(null=True, verbose_name="memory usage")), + ("points", models.FloatField(null=True, verbose_name="points granted")), + ("total", models.FloatField(null=True, verbose_name="points possible")), + ("batch", models.IntegerField(null=True, verbose_name="batch number")), + ( + "feedback", + models.CharField( + blank=True, max_length=50, verbose_name="judging feedback" + ), + ), + ( + "extended_feedback", + models.TextField( + blank=True, verbose_name="extended judging feedback" + ), + ), + ("output", models.TextField(blank=True, verbose_name="program output")), + ( + "submission", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="test_cases", + to="judge.Submission", + verbose_name="associated submission", + ), + ), ], options={ - 'verbose_name_plural': 'submission test cases', - 'verbose_name': 'submission test case', + "verbose_name_plural": "submission test cases", + "verbose_name": "submission test case", }, ), migrations.CreateModel( - name='Ticket', + name="Ticket", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='ticket title')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), - ('notes', models.TextField(blank=True, help_text='Staff notes for this issue to aid in processing.', verbose_name='quick notes')), - ('object_id', models.PositiveIntegerField(verbose_name='linked item ID')), - ('is_open', models.BooleanField(default=True, verbose_name='is ticket open?')), - ('assignees', models.ManyToManyField(related_name='assigned_tickets', to='judge.Profile', verbose_name='assignees')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='linked item type')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='judge.Profile', verbose_name='ticket creator')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField(max_length=100, verbose_name="ticket title"), + ), + ( + "time", + models.DateTimeField( + auto_now_add=True, verbose_name="creation time" + ), + ), + ( + "notes", + models.TextField( + blank=True, + help_text="Staff notes for this issue to aid in processing.", + verbose_name="quick notes", + ), + ), + ( + "object_id", + models.PositiveIntegerField(verbose_name="linked item ID"), + ), + ( + "is_open", + models.BooleanField(default=True, verbose_name="is ticket open?"), + ), + ( + "assignees", + models.ManyToManyField( + related_name="assigned_tickets", + to="judge.Profile", + verbose_name="assignees", + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.ContentType", + verbose_name="linked item type", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="judge.Profile", + verbose_name="ticket creator", + ), + ), ], ), migrations.CreateModel( - name='TicketMessage', + name="TicketMessage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(verbose_name='message body')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='message time')), - ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', related_query_name='message', to='judge.Ticket', verbose_name='ticket')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to='judge.Profile', verbose_name='poster')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("body", models.TextField(verbose_name="message body")), + ( + "time", + models.DateTimeField( + auto_now_add=True, verbose_name="message time" + ), + ), + ( + "ticket", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="messages", + related_query_name="message", + to="judge.Ticket", + verbose_name="ticket", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ticket_messages", + to="judge.Profile", + verbose_name="poster", + ), + ), ], ), migrations.AddField( - model_name='problem', - name='authors', - field=models.ManyToManyField(blank=True, related_name='authored_problems', to='judge.Profile', verbose_name='creators'), + model_name="problem", + name="authors", + field=models.ManyToManyField( + blank=True, + related_name="authored_problems", + to="judge.Profile", + verbose_name="creators", + ), ), migrations.AddField( - model_name='problem', - name='banned_users', - field=models.ManyToManyField(blank=True, help_text='Bans the selected users from submitting to this problem.', to='judge.Profile', verbose_name='personae non gratae'), + model_name="problem", + name="banned_users", + field=models.ManyToManyField( + blank=True, + help_text="Bans the selected users from submitting to this problem.", + to="judge.Profile", + verbose_name="personae non gratae", + ), ), migrations.AddField( - model_name='problem', - name='curators', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit a problem, but not be publicly shown as an author.', related_name='curated_problems', to='judge.Profile', verbose_name='curators'), + model_name="problem", + name="curators", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit a problem, but not be publicly shown as an author.", + related_name="curated_problems", + to="judge.Profile", + verbose_name="curators", + ), ), migrations.AddField( - model_name='problem', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.ProblemGroup', verbose_name='problem group'), + model_name="problem", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.ProblemGroup", + verbose_name="problem group", + ), ), migrations.AddField( - model_name='problem', - name='license', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.License'), + model_name="problem", + name="license", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="judge.License", + ), ), migrations.AddField( - model_name='problem', - name='organizations', - field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the problem.', to='judge.Organization', verbose_name='organizations'), + model_name="problem", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these organizations may see the problem.", + to="judge.Organization", + verbose_name="organizations", + ), ), migrations.AddField( - model_name='problem', - name='testers', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view a private problem, but not edit it.', related_name='tested_problems', to='judge.Profile', verbose_name='testers'), + model_name="problem", + name="testers", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view a private problem, but not edit it.", + related_name="tested_problems", + to="judge.Profile", + verbose_name="testers", + ), ), migrations.AddField( - model_name='problem', - name='types', - field=models.ManyToManyField(to='judge.ProblemType', verbose_name='problem types'), + model_name="problem", + name="types", + field=models.ManyToManyField( + to="judge.ProblemType", verbose_name="problem types" + ), ), migrations.AddField( - model_name='privatemessage', - name='sender', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='judge.Profile', verbose_name='sender'), + model_name="privatemessage", + name="sender", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sent_messages", + to="judge.Profile", + verbose_name="sender", + ), ), migrations.AddField( - model_name='privatemessage', - name='target', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to='judge.Profile', verbose_name='target'), + model_name="privatemessage", + name="target", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_messages", + to="judge.Profile", + verbose_name="target", + ), ), migrations.AddField( - model_name='organizationrequest', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='judge.Profile', verbose_name='user'), + model_name="organizationrequest", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="requests", + to="judge.Profile", + verbose_name="user", + ), ), migrations.AddField( - model_name='organization', - name='admins', - field=models.ManyToManyField(help_text='Those who can edit this organization', related_name='admin_of', to='judge.Profile', verbose_name='administrators'), + model_name="organization", + name="admins", + field=models.ManyToManyField( + help_text="Those who can edit this organization", + related_name="admin_of", + to="judge.Profile", + verbose_name="administrators", + ), ), migrations.AddField( - model_name='organization', - name='registrant', - field=models.ForeignKey(help_text='User who registered this organization', on_delete=django.db.models.deletion.CASCADE, related_name='registrant+', to='judge.Profile', verbose_name='registrant'), + model_name="organization", + name="registrant", + field=models.ForeignKey( + help_text="User who registered this organization", + on_delete=django.db.models.deletion.CASCADE, + related_name="registrant+", + to="judge.Profile", + verbose_name="registrant", + ), ), migrations.AddField( - model_name='languagelimit', - name='problem', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='language_limits', to='judge.Problem', verbose_name='problem'), + model_name="languagelimit", + name="problem", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="language_limits", + to="judge.Problem", + verbose_name="problem", + ), ), migrations.AddField( - model_name='judge', - name='problems', - field=models.ManyToManyField(related_name='judges', to='judge.Problem', verbose_name='problems'), + model_name="judge", + name="problems", + field=models.ManyToManyField( + related_name="judges", to="judge.Problem", verbose_name="problems" + ), ), migrations.AddField( - model_name='judge', - name='runtimes', - field=models.ManyToManyField(related_name='judges', to='judge.Language', verbose_name='judges'), + model_name="judge", + name="runtimes", + field=models.ManyToManyField( + related_name="judges", to="judge.Language", verbose_name="judges" + ), ), migrations.AddField( - model_name='contestsubmission', - name='submission', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='contest', to='judge.Submission', verbose_name='submission'), + model_name="contestsubmission", + name="submission", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="contest", + to="judge.Submission", + verbose_name="submission", + ), ), migrations.AddField( - model_name='contestproblem', - name='problem', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contests', to='judge.Problem', verbose_name='problem'), + model_name="contestproblem", + name="problem", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contests", + to="judge.Problem", + verbose_name="problem", + ), ), migrations.AddField( - model_name='contestparticipation', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_history', to='judge.Profile', verbose_name='user'), + model_name="contestparticipation", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contest_history", + to="judge.Profile", + verbose_name="user", + ), ), migrations.AddField( - model_name='contest', - name='banned_users', - field=models.ManyToManyField(blank=True, help_text='Bans the selected users from joining this contest.', to='judge.Profile', verbose_name='personae non gratae'), + model_name="contest", + name="banned_users", + field=models.ManyToManyField( + blank=True, + help_text="Bans the selected users from joining this contest.", + to="judge.Profile", + verbose_name="personae non gratae", + ), ), migrations.AddField( - model_name='contest', - name='organizations', - field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the contest', to='judge.Organization', verbose_name='organizations'), + model_name="contest", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these organizations may see the contest", + to="judge.Organization", + verbose_name="organizations", + ), ), migrations.AddField( - model_name='contest', - name='organizers', - field=models.ManyToManyField(help_text='These people will be able to edit the contest.', related_name='_contest_organizers_+', to='judge.Profile'), + model_name="contest", + name="organizers", + field=models.ManyToManyField( + help_text="These people will be able to edit the contest.", + related_name="_contest_organizers_+", + to="judge.Profile", + ), ), migrations.AddField( - model_name='contest', - name='problems', - field=models.ManyToManyField(through='judge.ContestProblem', to='judge.Problem', verbose_name='problems'), + model_name="contest", + name="problems", + field=models.ManyToManyField( + through="judge.ContestProblem", + to="judge.Problem", + verbose_name="problems", + ), ), migrations.AddField( - model_name='contest', - name='rate_exclude', - field=models.ManyToManyField(blank=True, related_name='_contest_rate_exclude_+', to='judge.Profile', verbose_name='exclude from ratings'), + model_name="contest", + name="rate_exclude", + field=models.ManyToManyField( + blank=True, + related_name="_contest_rate_exclude_+", + to="judge.Profile", + verbose_name="exclude from ratings", + ), ), migrations.AddField( - model_name='contest', - name='tags', - field=models.ManyToManyField(blank=True, related_name='contests', to='judge.ContestTag', verbose_name='contest tags'), + model_name="contest", + name="tags", + field=models.ManyToManyField( + blank=True, + related_name="contests", + to="judge.ContestTag", + verbose_name="contest tags", + ), ), migrations.AddField( - model_name='commentvote', - name='voter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voted_comments', to='judge.Profile'), + model_name="commentvote", + name="voter", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="voted_comments", + to="judge.Profile", + ), ), migrations.AddField( - model_name='comment', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='commenter'), + model_name="comment", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Profile", + verbose_name="commenter", + ), ), migrations.AddField( - model_name='comment', - name='parent', - field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='judge.Comment', verbose_name='parent'), + model_name="comment", + name="parent", + field=mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="replies", + to="judge.Comment", + verbose_name="parent", + ), ), migrations.AddField( - model_name='blogpost', - name='authors', - field=models.ManyToManyField(blank=True, to='judge.Profile', verbose_name='authors'), + model_name="blogpost", + name="authors", + field=models.ManyToManyField( + blank=True, to="judge.Profile", verbose_name="authors" + ), ), migrations.AlterUniqueTogether( - name='rating', - unique_together=set([('user', 'contest')]), + name="rating", + unique_together=set([("user", "contest")]), ), migrations.AlterUniqueTogether( - name='problemtranslation', - unique_together=set([('problem', 'language')]), + name="problemtranslation", + unique_together=set([("problem", "language")]), ), migrations.AlterUniqueTogether( - name='languagelimit', - unique_together=set([('problem', 'language')]), + name="languagelimit", + unique_together=set([("problem", "language")]), ), migrations.AlterUniqueTogether( - name='contestproblem', - unique_together=set([('problem', 'contest')]), + name="contestproblem", + unique_together=set([("problem", "contest")]), ), migrations.AlterUniqueTogether( - name='contestparticipation', - unique_together=set([('contest', 'user', 'virtual')]), + name="contestparticipation", + unique_together=set([("contest", "user", "virtual")]), ), migrations.AlterUniqueTogether( - name='commentvote', - unique_together=set([('voter', 'comment')]), + name="commentvote", + unique_together=set([("voter", "comment")]), ), ] diff --git a/judge/migrations/0085_submission_source.py b/judge/migrations/0085_submission_source.py index d094efd..3f0dd18 100644 --- a/judge/migrations/0085_submission_source.py +++ b/judge/migrations/0085_submission_source.py @@ -7,33 +7,61 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0084_contest_formats'), + ("judge", "0084_contest_formats"), ] operations = [ migrations.CreateModel( - name='SubmissionSource', + name="SubmissionSource", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('source', models.TextField(max_length=65536, verbose_name='source code')), - ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='link', to='judge.Submission', verbose_name='associated submission')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "source", + models.TextField(max_length=65536, verbose_name="source code"), + ), + ( + "submission", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="link", + to="judge.Submission", + verbose_name="associated submission", + ), + ), ], ), migrations.RunSQL( - ['''INSERT INTO judge_submissionsource (source, submission_id) - SELECT source, id AS 'submission_id' FROM judge_submission;'''], - ['''UPDATE judge_submission sub + [ + """INSERT INTO judge_submissionsource (source, submission_id) + SELECT source, id AS 'submission_id' FROM judge_submission;""" + ], + [ + """UPDATE judge_submission sub INNER JOIN judge_submissionsource src ON sub.id = src.submission_id - SET sub.source = src.source;'''], + SET sub.source = src.source;""" + ], elidable=True, ), migrations.RemoveField( - model_name='submission', - name='source', + model_name="submission", + name="source", ), migrations.AlterField( - model_name='submissionsource', - name='submission', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='source', to='judge.Submission', verbose_name='associated submission'), + model_name="submissionsource", + name="submission", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="source", + to="judge.Submission", + verbose_name="associated submission", + ), ), ] diff --git a/judge/migrations/0086_rating_ceiling.py b/judge/migrations/0086_rating_ceiling.py index a544a21..d529ac1 100644 --- a/judge/migrations/0086_rating_ceiling.py +++ b/judge/migrations/0086_rating_ceiling.py @@ -6,18 +6,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0085_submission_source'), + ("judge", "0085_submission_source"), ] operations = [ migrations.AddField( - model_name='contest', - name='rating_ceiling', - field=models.IntegerField(blank=True, help_text='Rating ceiling for contest', null=True, verbose_name='rating ceiling'), + model_name="contest", + name="rating_ceiling", + field=models.IntegerField( + blank=True, + help_text="Rating ceiling for contest", + null=True, + verbose_name="rating ceiling", + ), ), migrations.AddField( - model_name='contest', - name='rating_floor', - field=models.IntegerField(blank=True, help_text='Rating floor for contest', null=True, verbose_name='rating floor'), + model_name="contest", + name="rating_floor", + field=models.IntegerField( + blank=True, + help_text="Rating floor for contest", + null=True, + verbose_name="rating floor", + ), ), ] diff --git a/judge/migrations/0087_problem_resource_limits.py b/judge/migrations/0087_problem_resource_limits.py index e6da153..c9fd18f 100644 --- a/judge/migrations/0087_problem_resource_limits.py +++ b/judge/migrations/0087_problem_resource_limits.py @@ -7,18 +7,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0086_rating_ceiling'), + ("judge", "0086_rating_ceiling"), ] operations = [ migrations.AlterField( - model_name='problem', - name='memory_limit', - field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit'), + model_name="problem", + name="memory_limit", + field=models.PositiveIntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).", + verbose_name="memory limit", + ), ), migrations.AlterField( - model_name='problem', - name='time_limit', - field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2000)], verbose_name='time limit'), + model_name="problem", + name="time_limit", + field=models.FloatField( + help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(2000), + ], + verbose_name="time limit", + ), ), ] diff --git a/judge/migrations/0088_private_contests.py b/judge/migrations/0088_private_contests.py index b3505b5..8a5b886 100644 --- a/judge/migrations/0088_private_contests.py +++ b/judge/migrations/0088_private_contests.py @@ -6,32 +6,53 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0087_problem_resource_limits'), + ("judge", "0087_problem_resource_limits"), ] 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'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + name="contest", + options={ + "permissions": ( + ("see_private_contest", "See private contests"), + ("edit_own_contest", "Edit own contests"), + ("edit_all_contest", "Edit all contests"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), migrations.RenameField( - model_name='contest', - old_name='is_public', - new_name='is_visible', + model_name="contest", + old_name="is_public", + new_name="is_visible", ), migrations.AddField( - model_name='contest', - name='is_organization_private', - field=models.BooleanField(default=False, verbose_name='private to organizations'), + model_name="contest", + name="is_organization_private", + field=models.BooleanField( + default=False, verbose_name="private to organizations" + ), ), migrations.AddField( - model_name='contest', - name='private_contestants', - field=models.ManyToManyField(blank=True, help_text='If private, only these users may see the contest', related_name='_contest_private_contestants_+', to='judge.Profile', verbose_name='private contestants'), + model_name="contest", + name="private_contestants", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these users may see the contest", + related_name="_contest_private_contestants_+", + to="judge.Profile", + verbose_name="private contestants", + ), ), migrations.AlterField( - model_name='contest', - name='is_private', - field=models.BooleanField(default=False, verbose_name='private to specific users'), + model_name="contest", + name="is_private", + field=models.BooleanField( + default=False, verbose_name="private to specific users" + ), ), ] diff --git a/judge/migrations/0089_submission_to_contest.py b/judge/migrations/0089_submission_to_contest.py index 5d464cb..f1cb283 100644 --- a/judge/migrations/0089_submission_to_contest.py +++ b/judge/migrations/0089_submission_to_contest.py @@ -7,21 +7,31 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0088_private_contests'), + ("judge", "0088_private_contests"), ] operations = [ migrations.AddField( - model_name='submission', - name='contest_object', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.Contest', verbose_name='contest'), + model_name="submission", + name="contest_object", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="judge.Contest", + verbose_name="contest", + ), ), - migrations.RunSQL(''' + migrations.RunSQL( + """ UPDATE `judge_submission` INNER JOIN `judge_contestsubmission` ON (`judge_submission`.`id` = `judge_contestsubmission`.`submission_id`) INNER JOIN `judge_contestparticipation` ON (`judge_contestsubmission`.`participation_id` = `judge_contestparticipation`.`id`) SET `judge_submission`.`contest_object_id` = `judge_contestparticipation`.`contest_id` - ''', migrations.RunSQL.noop), + """, + migrations.RunSQL.noop, + ), ] diff --git a/judge/migrations/0090_fix_contest_visibility.py b/judge/migrations/0090_fix_contest_visibility.py index 10480fb..8e89949 100644 --- a/judge/migrations/0090_fix_contest_visibility.py +++ b/judge/migrations/0090_fix_contest_visibility.py @@ -4,16 +4,19 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0089_submission_to_contest'), + ("judge", "0089_submission_to_contest"), ] operations = [ - migrations.RunSQL(''' + migrations.RunSQL( + """ UPDATE `judge_contest` SET `judge_contest`.`is_private` = 0, `judge_contest`.`is_organization_private` = 1 WHERE `judge_contest`.`is_private` = 1 - ''', ''' + """, + """ UPDATE `judge_contest` SET `judge_contest`.`is_private` = `judge_contest`.`is_organization_private` - '''), + """, + ), ] diff --git a/judge/migrations/0091_compiler_message_ansi2html.py b/judge/migrations/0091_compiler_message_ansi2html.py index 7240f95..32db431 100644 --- a/judge/migrations/0091_compiler_message_ansi2html.py +++ b/judge/migrations/0091_compiler_message_ansi2html.py @@ -5,17 +5,17 @@ from lxml.html.clean import clean_html def strip_error_html(apps, schema_editor): - Submission = apps.get_model('judge', 'Submission') + Submission = apps.get_model("judge", "Submission") for sub in Submission.objects.filter(error__isnull=False).iterator(): if sub.error: sub.error = clean_html(lh.fromstring(sub.error)).text_content() - sub.save(update_fields=['error']) + sub.save(update_fields=["error"]) class Migration(migrations.Migration): dependencies = [ - ('judge', '0090_fix_contest_visibility'), + ("judge", "0090_fix_contest_visibility"), ] operations = [ diff --git a/judge/migrations/0092_contest_clone.py b/judge/migrations/0092_contest_clone.py index 0235513..0982ee8 100644 --- a/judge/migrations/0092_contest_clone.py +++ b/judge/migrations/0092_contest_clone.py @@ -6,12 +6,24 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0091_compiler_message_ansi2html'), + ("judge", "0091_compiler_message_ansi2html"), ] 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'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + 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"), + ("contest_rating", "Rate contests"), + ("contest_access_code", "Contest access codes"), + ("create_private_contest", "Create private contests"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), ] diff --git a/judge/migrations/0093_contest_moss.py b/judge/migrations/0093_contest_moss.py index 10e2799..0dec938 100644 --- a/judge/migrations/0093_contest_moss.py +++ b/judge/migrations/0093_contest_moss.py @@ -7,39 +7,70 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0092_contest_clone'), + ("judge", "0092_contest_clone"), ] operations = [ migrations.CreateModel( - name='ContestMoss', + name="ContestMoss", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language', models.CharField(max_length=10)), - ('submission_count', models.PositiveIntegerField(default=0)), - ('url', models.URLField(blank=True, null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("language", models.CharField(max_length=10)), + ("submission_count", models.PositiveIntegerField(default=0)), + ("url", models.URLField(blank=True, null=True)), ], options={ - 'verbose_name': 'contest moss result', - 'verbose_name_plural': 'contest moss results', + "verbose_name": "contest moss result", + "verbose_name_plural": "contest moss results", }, ), 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')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + 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"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), migrations.AddField( - model_name='contestmoss', - name='contest', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Contest', verbose_name='contest'), + model_name="contestmoss", + name="contest", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moss", + to="judge.Contest", + verbose_name="contest", + ), ), migrations.AddField( - model_name='contestmoss', - name='problem', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Problem', verbose_name='problem'), + model_name="contestmoss", + name="problem", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moss", + to="judge.Problem", + verbose_name="problem", + ), ), migrations.AlterUniqueTogether( - name='contestmoss', - unique_together={('contest', 'problem', 'language')}, + name="contestmoss", + unique_together={("contest", "problem", "language")}, ), ] diff --git a/judge/migrations/0094_submissiontestcase_unique_together.py b/judge/migrations/0094_submissiontestcase_unique_together.py index 8341329..c56cded 100644 --- a/judge/migrations/0094_submissiontestcase_unique_together.py +++ b/judge/migrations/0094_submissiontestcase_unique_together.py @@ -3,12 +3,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0093_contest_moss'), + ("judge", "0093_contest_moss"), ] operations = [ migrations.AlterUniqueTogether( - name='submissiontestcase', - unique_together={('submission', 'case')}, + name="submissiontestcase", + unique_together={("submission", "case")}, ), ] diff --git a/judge/migrations/0095_organization_logo_override.py b/judge/migrations/0095_organization_logo_override.py index b1d86eb..6db6499 100644 --- a/judge/migrations/0095_organization_logo_override.py +++ b/judge/migrations/0095_organization_logo_override.py @@ -6,13 +6,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0094_submissiontestcase_unique_together'), + ("judge", "0094_submissiontestcase_unique_together"), ] operations = [ migrations.AddField( - model_name='organization', - name='logo_override_image', - field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users viewing the organization.', max_length=150, verbose_name='Logo override image'), + model_name="organization", + name="logo_override_image", + field=models.CharField( + blank=True, + default="", + help_text="This image will replace the default site logo for users viewing the organization.", + max_length=150, + verbose_name="Logo override image", + ), ), ] diff --git a/judge/migrations/0096_profile_language_set_default.py b/judge/migrations/0096_profile_language_set_default.py index a0a9453..1468f78 100644 --- a/judge/migrations/0096_profile_language_set_default.py +++ b/judge/migrations/0096_profile_language_set_default.py @@ -7,21 +7,26 @@ 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] + Language = apps.get_model("judge", "Language") + Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[0] class Migration(migrations.Migration): dependencies = [ - ('judge', '0095_organization_logo_override'), + ("judge", "0095_organization_logo_override"), ] operations = [ migrations.RunPython(create_python3, reverse_code=migrations.RunPython.noop), migrations.AlterField( - model_name='profile', - name='language', - field=models.ForeignKey(default=judge.models.runtime.Language.get_default_language_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='judge.Language', verbose_name='preferred language'), + model_name="profile", + name="language", + field=models.ForeignKey( + default=judge.models.runtime.Language.get_default_language_pk, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="judge.Language", + verbose_name="preferred language", + ), ), ] diff --git a/judge/migrations/0097_participation_is_disqualified.py b/judge/migrations/0097_participation_is_disqualified.py index 537f83f..3d3f367 100644 --- a/judge/migrations/0097_participation_is_disqualified.py +++ b/judge/migrations/0097_participation_is_disqualified.py @@ -6,18 +6,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0096_profile_language_set_default'), + ("judge", "0096_profile_language_set_default"), ] operations = [ migrations.AddField( - model_name='contestparticipation', - name='is_disqualified', - field=models.BooleanField(default=False, help_text='Whether this participation is disqualified.', verbose_name='is disqualified'), + model_name="contestparticipation", + name="is_disqualified", + field=models.BooleanField( + default=False, + help_text="Whether this participation is disqualified.", + verbose_name="is disqualified", + ), ), migrations.AlterField( - model_name='contestparticipation', - name='virtual', - field=models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation.', verbose_name='virtual participation id'), + model_name="contestparticipation", + name="virtual", + field=models.IntegerField( + default=0, + help_text="0 means non-virtual, otherwise the n-th virtual participation.", + verbose_name="virtual participation id", + ), ), ] diff --git a/judge/migrations/0098_auto_20200123_2136.py b/judge/migrations/0098_auto_20200123_2136.py index 333c67c..3093cd3 100644 --- a/judge/migrations/0098_auto_20200123_2136.py +++ b/judge/migrations/0098_auto_20200123_2136.py @@ -8,148 +8,961 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0097_participation_is_disqualified'), + ("judge", "0097_participation_is_disqualified"), ] operations = [ migrations.AlterField( - model_name='comment', - name='level', + model_name="comment", + name="level", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='comment', - name='lft', + model_name="comment", + name="lft", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='comment', - name='rght', + model_name="comment", + name="rght", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='contest', - name='format_name', - field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('ioi', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'), + model_name="contest", + name="format_name", + field=models.CharField( + choices=[ + ("atcoder", "AtCoder"), + ("default", "Default"), + ("ecoo", "ECOO"), + ("ioi", "IOI"), + ], + default="default", + help_text="The contest format module to use.", + max_length=32, + verbose_name="contest format", + ), ), migrations.AlterField( - model_name='judge', - name='auth_key', - field=models.CharField(help_text='A key to authenticate this judge', max_length=100, verbose_name='authentication key'), + model_name="judge", + name="auth_key", + field=models.CharField( + help_text="A key to authenticate this judge", + max_length=100, + verbose_name="authentication key", + ), ), migrations.AlterField( - model_name='language', - name='description', - field=models.TextField(blank=True, help_text='Use this field to inform users of quirks with your environment, additional restrictions, etc.', verbose_name='language description'), + model_name="language", + name="description", + field=models.TextField( + blank=True, + help_text="Use this field to inform users of quirks with your environment, additional restrictions, etc.", + verbose_name="language description", + ), ), migrations.AlterField( - model_name='languagelimit', - name='memory_limit', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'), + model_name="languagelimit", + name="memory_limit", + field=models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1048576), + ], + verbose_name="memory limit", + ), ), migrations.AlterField( - model_name='languagelimit', - name='time_limit', - field=models.FloatField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='time limit'), + model_name="languagelimit", + name="time_limit", + field=models.FloatField( + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(60), + ], + verbose_name="time limit", + ), ), migrations.AlterField( - model_name='navigationbar', - name='level', + model_name="navigationbar", + name="level", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='navigationbar', - name='lft', + model_name="navigationbar", + name="lft", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='navigationbar', - name='rght', + model_name="navigationbar", + name="rght", field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( - model_name='problem', - name='allowed_languages', - field=models.ManyToManyField(help_text='List of allowed submission languages.', to='judge.Language', verbose_name='allowed languages'), + model_name="problem", + name="allowed_languages", + field=models.ManyToManyField( + help_text="List of allowed submission languages.", + to="judge.Language", + verbose_name="allowed languages", + ), ), migrations.AlterField( - model_name='problem', - name='authors', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the problem, and be listed as authors.', related_name='authored_problems', to='judge.Profile', verbose_name='creators'), + model_name="problem", + name="authors", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit the problem, and be listed as authors.", + related_name="authored_problems", + to="judge.Profile", + verbose_name="creators", + ), ), migrations.AlterField( - model_name='problem', - name='code', - field=models.CharField(help_text='A short, unique code for the problem, used in the url after /problem/', max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Problem code must be ^[a-z0-9]+$')], verbose_name='problem code'), + model_name="problem", + name="code", + field=models.CharField( + help_text="A short, unique code for the problem, used in the url after /problem/", + max_length=20, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-z0-9]+$", "Problem code must be ^[a-z0-9]+$" + ) + ], + verbose_name="problem code", + ), ), migrations.AlterField( - model_name='problem', - name='curators', - field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the problem, but not be listed as authors.', related_name='curated_problems', to='judge.Profile', verbose_name='curators'), + model_name="problem", + name="curators", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to edit the problem, but not be listed as authors.", + related_name="curated_problems", + to="judge.Profile", + verbose_name="curators", + ), ), migrations.AlterField( - model_name='problem', - name='group', - field=models.ForeignKey(help_text='The group of problem, shown under Category in the problem list.', on_delete=django.db.models.deletion.CASCADE, to='judge.ProblemGroup', verbose_name='problem group'), + model_name="problem", + name="group", + field=models.ForeignKey( + help_text="The group of problem, shown under Category in the problem list.", + on_delete=django.db.models.deletion.CASCADE, + to="judge.ProblemGroup", + verbose_name="problem group", + ), ), migrations.AlterField( - model_name='problem', - name='is_manually_managed', - field=models.BooleanField(db_index=True, default=False, help_text='Whether judges should be allowed to manage data or not.', verbose_name='manually managed'), + model_name="problem", + name="is_manually_managed", + field=models.BooleanField( + db_index=True, + default=False, + help_text="Whether judges should be allowed to manage data or not.", + verbose_name="manually managed", + ), ), migrations.AlterField( - model_name='problem', - name='license', - field=models.ForeignKey(blank=True, help_text='The license under which this problem is published.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.License'), + model_name="problem", + name="license", + field=models.ForeignKey( + blank=True, + help_text="The license under which this problem is published.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="judge.License", + ), ), migrations.AlterField( - model_name='problem', - name='memory_limit', - field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'), + model_name="problem", + name="memory_limit", + field=models.PositiveIntegerField( + help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1048576), + ], + verbose_name="memory limit", + ), ), migrations.AlterField( - model_name='problem', - name='name', - field=models.CharField(db_index=True, help_text='The full name of the problem, as shown in the problem list.', max_length=100, verbose_name='problem name'), + model_name="problem", + name="name", + field=models.CharField( + db_index=True, + help_text="The full name of the problem, as shown in the problem list.", + max_length=100, + verbose_name="problem name", + ), ), migrations.AlterField( - model_name='problem', - name='points', - field=models.FloatField(help_text="Points awarded for problem completion. Points are displayed with a 'p' suffix if partial.", validators=[django.core.validators.MinValueValidator(0)], verbose_name='points'), + model_name="problem", + name="points", + field=models.FloatField( + help_text="Points awarded for problem completion. Points are displayed with a 'p' suffix if partial.", + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="points", + ), ), migrations.AlterField( - model_name='problem', - name='testers', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view the private problem, but not edit it.', related_name='tested_problems', to='judge.Profile', verbose_name='testers'), + model_name="problem", + name="testers", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view the private problem, but not edit it.", + related_name="tested_problems", + to="judge.Profile", + verbose_name="testers", + ), ), migrations.AlterField( - model_name='problem', - name='time_limit', - field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='time limit'), + model_name="problem", + name="time_limit", + field=models.FloatField( + help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(60), + ], + verbose_name="time limit", + ), ), migrations.AlterField( - model_name='problem', - name='types', - field=models.ManyToManyField(help_text="The type of problem, as shown on the problem's page.", to='judge.ProblemType', verbose_name='problem types'), + model_name="problem", + name="types", + field=models.ManyToManyField( + help_text="The type of problem, as shown on the problem's page.", + to="judge.ProblemType", + verbose_name="problem types", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('checker.py', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("checker.py", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('checker.py', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("checker.py", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtranslation', - name='language', - field=models.CharField(choices=[('de', 'German'), ('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ja', 'Japanese'), ('ko', 'Korean'), ('pt', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sr-latn', 'Serbian (Latin)'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese')], max_length=7, verbose_name='language'), + model_name="problemtranslation", + name="language", + field=models.CharField( + choices=[ + ("de", "German"), + ("en", "English"), + ("es", "Spanish"), + ("fr", "French"), + ("hr", "Croatian"), + ("hu", "Hungarian"), + ("ja", "Japanese"), + ("ko", "Korean"), + ("pt", "Brazilian Portuguese"), + ("ro", "Romanian"), + ("ru", "Russian"), + ("sr-latn", "Serbian (Latin)"), + ("tr", "Turkish"), + ("vi", "Vietnamese"), + ("zh-hans", "Simplified Chinese"), + ("zh-hant", "Traditional Chinese"), + ], + max_length=7, + verbose_name="language", + ), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Mexico_City', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="America/Mexico_City", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0099_custom_checker.py b/judge/migrations/0099_custom_checker.py index b2552b7..24ab46b 100644 --- a/judge/migrations/0099_custom_checker.py +++ b/judge/migrations/0099_custom_checker.py @@ -8,23 +8,59 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0098_auto_20200123_2136'), + ("judge", "0098_auto_20200123_2136"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='custom_checker', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='custom checker file'), + model_name="problemdata", + name="custom_checker", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + verbose_name="custom checker file", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0100_auto_20200127_0059.py b/judge/migrations/0100_auto_20200127_0059.py index e070f7a..5d1cd91 100644 --- a/judge/migrations/0100_auto_20200127_0059.py +++ b/judge/migrations/0100_auto_20200127_0059.py @@ -9,13 +9,24 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0099_custom_checker'), + ("judge", "0099_custom_checker"), ] operations = [ migrations.AlterField( - model_name='problemdata', - name='custom_checker', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['py'])], verbose_name='custom checker file'), + model_name="problemdata", + name="custom_checker", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["py"] + ) + ], + verbose_name="custom checker file", + ), ), ] diff --git a/judge/migrations/0101_custom_validator.py b/judge/migrations/0101_custom_validator.py index 842006c..8575ce1 100644 --- a/judge/migrations/0101_custom_validator.py +++ b/judge/migrations/0101_custom_validator.py @@ -9,28 +9,727 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0100_auto_20200127_0059'), + ("judge", "0100_auto_20200127_0059"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='custom_valid', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['cpp'])], verbose_name='custom validator file'), + model_name="problemdata", + name="custom_valid", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["cpp"] + ) + ], + verbose_name="custom validator file", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('custom_valid', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("custom_valid", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('custom_valid', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("custom_valid", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='Asia/Ho_Chi_Minh', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="Asia/Ho_Chi_Minh", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0102_fix_custom_validator.py b/judge/migrations/0102_fix_custom_validator.py index a0dccce..e6bcd4a 100644 --- a/judge/migrations/0102_fix_custom_validator.py +++ b/judge/migrations/0102_fix_custom_validator.py @@ -6,18 +6,50 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0101_custom_validator'), + ("judge", "0101_custom_validator"), ] operations = [ migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('customval', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("customval", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker'), ('customval', 'Custom Validator')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker"), + ("customval", "Custom Validator"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0103_fix_custom_validator.py b/judge/migrations/0103_fix_custom_validator.py index 2720bb8..d354303 100644 --- a/judge/migrations/0103_fix_custom_validator.py +++ b/judge/migrations/0103_fix_custom_validator.py @@ -6,13 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0102_fix_custom_validator'), + ("judge", "0102_fix_custom_validator"), ] operations = [ migrations.RenameField( - model_name='problemdata', - old_name='custom_valid', - new_name='custom_validator', + model_name="problemdata", + old_name="custom_valid", + new_name="custom_validator", ), ] diff --git a/judge/migrations/0104_auto_20200410_1313.py b/judge/migrations/0104_auto_20200410_1313.py index bbefe21..ccba53d 100644 --- a/judge/migrations/0104_auto_20200410_1313.py +++ b/judge/migrations/0104_auto_20200410_1313.py @@ -6,23 +6,57 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0103_fix_custom_validator'), + ("judge", "0103_fix_custom_validator"), ] operations = [ migrations.AlterField( - model_name='contestproblem', - name='output_prefix_override', - field=models.IntegerField(blank=True, default=0, null=True, verbose_name='visible testcases'), + model_name="contestproblem", + name="output_prefix_override", + field=models.IntegerField( + blank=True, default=0, null=True, verbose_name="visible testcases" + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0105_auto_20200523_0756.py b/judge/migrations/0105_auto_20200523_0756.py index 79ccb4e..553c24f 100644 --- a/judge/migrations/0105_auto_20200523_0756.py +++ b/judge/migrations/0105_auto_20200523_0756.py @@ -6,18 +6,681 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0104_auto_20200410_1313'), + ("judge", "0104_auto_20200410_1313"), ] operations = [ migrations.AlterField( - model_name='contestproblem', - name='output_prefix_override', - field=models.IntegerField(blank=True, default=0, help_text='0 to not show testcases, 1 to show', null=True, verbose_name='visible testcases'), + model_name="contestproblem", + name="output_prefix_override", + field=models.IntegerField( + blank=True, + default=0, + help_text="0 to not show testcases, 1 to show", + null=True, + verbose_name="visible testcases", + ), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Nuuk', 'Nuuk'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='Asia/Ho_Chi_Minh', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Nuuk", "Nuuk"), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="Asia/Ho_Chi_Minh", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0106_friend.py b/judge/migrations/0106_friend.py index e820242..1235013 100644 --- a/judge/migrations/0106_friend.py +++ b/judge/migrations/0106_friend.py @@ -7,16 +7,31 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0105_auto_20200523_0756'), + ("judge", "0105_auto_20200523_0756"), ] operations = [ migrations.CreateModel( - name='Friend', + name="Friend", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('current_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_users', to='judge.Profile')), - ('users', models.ManyToManyField(to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "current_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="following_users", + to="judge.Profile", + ), + ), + ("users", models.ManyToManyField(to="judge.Profile")), ], ), ] diff --git a/judge/migrations/0107_notification.py b/judge/migrations/0107_notification.py index d8857d6..9539711 100644 --- a/judge/migrations/0107_notification.py +++ b/judge/migrations/0107_notification.py @@ -7,19 +7,45 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0106_friend'), + ("judge", "0106_friend"), ] operations = [ migrations.CreateModel( - name='Notification', + name="Notification", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')), - ('read', models.BooleanField(default=False, verbose_name='read')), - ('category', models.CharField(max_length=10, verbose_name='category')), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Comment', verbose_name='comment')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='judge.Profile', verbose_name='owner')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField(auto_now_add=True, verbose_name="posted time"), + ), + ("read", models.BooleanField(default=False, verbose_name="read")), + ("category", models.CharField(max_length=10, verbose_name="category")), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="judge.Comment", + verbose_name="comment", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to="judge.Profile", + verbose_name="owner", + ), + ), ], ), ] diff --git a/judge/migrations/0108_submission_judged_date.py b/judge/migrations/0108_submission_judged_date.py index 5794ace..e58ca6b 100644 --- a/judge/migrations/0108_submission_judged_date.py +++ b/judge/migrations/0108_submission_judged_date.py @@ -6,13 +6,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0107_notification'), + ("judge", "0107_notification"), ] operations = [ migrations.AddField( - model_name='submission', - name='judged_date', - field=models.DateTimeField(default=None, null=True, verbose_name='submission judge time'), + model_name="submission", + name="judged_date", + field=models.DateTimeField( + default=None, null=True, verbose_name="submission judge time" + ), ), ] diff --git a/judge/migrations/0109_auto_20201017_1151.py b/judge/migrations/0109_auto_20201017_1151.py index 9bce543..3e94df0 100644 --- a/judge/migrations/0109_auto_20201017_1151.py +++ b/judge/migrations/0109_auto_20201017_1151.py @@ -7,18 +7,27 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0108_submission_judged_date'), + ("judge", "0108_submission_judged_date"), ] operations = [ migrations.AddField( - model_name='notification', - name='html_link', - field=models.TextField(default='', max_length=1000, verbose_name='html link to comments, used for non-comments'), + model_name="notification", + name="html_link", + field=models.TextField( + default="", + max_length=1000, + verbose_name="html link to comments, used for non-comments", + ), ), migrations.AlterField( - model_name='notification', - name='comment', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='judge.Comment', verbose_name='comment'), + model_name="notification", + name="comment", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="judge.Comment", + verbose_name="comment", + ), ), ] diff --git a/judge/migrations/0110_notification_author.py b/judge/migrations/0110_notification_author.py index e0dec3e..367c96a 100644 --- a/judge/migrations/0110_notification_author.py +++ b/judge/migrations/0110_notification_author.py @@ -7,13 +7,18 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0109_auto_20201017_1151'), + ("judge", "0109_auto_20201017_1151"), ] operations = [ migrations.AddField( - model_name='notification', - name='author', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='who trigger, used for non-comment'), + model_name="notification", + name="author", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="judge.Profile", + verbose_name="who trigger, used for non-comment", + ), ), ] diff --git a/judge/migrations/0111_contest_decimal_points.py b/judge/migrations/0111_contest_decimal_points.py index 4dc3968..b83e3b5 100644 --- a/judge/migrations/0111_contest_decimal_points.py +++ b/judge/migrations/0111_contest_decimal_points.py @@ -7,18 +7,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0110_notification_author'), + ("judge", "0110_notification_author"), ] operations = [ migrations.AddField( - model_name='contest', - name='points_precision', - field=models.IntegerField(default=2, help_text='Number of digits to round points to.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)], verbose_name='precision points'), + model_name="contest", + name="points_precision", + field=models.IntegerField( + default=2, + help_text="Number of digits to round points to.", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(10), + ], + verbose_name="precision points", + ), ), migrations.AlterField( - model_name='contestparticipation', - name='score', - field=models.FloatField(db_index=True, default=0, verbose_name='score'), + model_name="contestparticipation", + name="score", + field=models.FloatField(db_index=True, default=0, verbose_name="score"), ), ] diff --git a/judge/migrations/0112_contest_view_contest_scoreboard.py b/judge/migrations/0112_contest_view_contest_scoreboard.py index 9a0dab2..00e2da4 100644 --- a/judge/migrations/0112_contest_view_contest_scoreboard.py +++ b/judge/migrations/0112_contest_view_contest_scoreboard.py @@ -6,13 +6,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0111_contest_decimal_points'), + ("judge", "0111_contest_decimal_points"), ] operations = [ migrations.AddField( - model_name='contest', - name='view_contest_scoreboard', - field=models.ManyToManyField(blank=True, help_text='These users will be able to view the scoreboard.', related_name='view_contest_scoreboard', to='judge.Profile', verbose_name='view contest scoreboard'), + model_name="contest", + name="view_contest_scoreboard", + field=models.ManyToManyField( + blank=True, + help_text="These users will be able to view the scoreboard.", + related_name="view_contest_scoreboard", + to="judge.Profile", + verbose_name="view contest scoreboard", + ), ), ] diff --git a/judge/migrations/0113_auto_20201228_0911.py b/judge/migrations/0113_auto_20201228_0911.py index f08350a..36d7dda 100644 --- a/judge/migrations/0113_auto_20201228_0911.py +++ b/judge/migrations/0113_auto_20201228_0911.py @@ -6,12 +6,26 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('judge', '0112_contest_view_contest_scoreboard'), + ("judge", "0112_contest_view_contest_scoreboard"), ] 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')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + 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"), + ), + "verbose_name": "contest", + "verbose_name_plural": "contests", + }, ), ] diff --git a/judge/migrations/0114_auto_20201228_1041.py b/judge/migrations/0114_auto_20201228_1041.py index 3103b3c..609c5d4 100644 --- a/judge/migrations/0114_auto_20201228_1041.py +++ b/judge/migrations/0114_auto_20201228_1041.py @@ -6,18 +6,25 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0113_auto_20201228_0911'), + ("judge", "0113_auto_20201228_0911"), ] operations = [ migrations.AddField( - model_name='blogpost', - name='is_organization_private', - field=models.BooleanField(default=False, verbose_name='private to organizations'), + model_name="blogpost", + name="is_organization_private", + field=models.BooleanField( + default=False, verbose_name="private to organizations" + ), ), migrations.AddField( - model_name='blogpost', - name='organizations', - field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the blog post.', to='judge.Organization', verbose_name='organizations'), + model_name="blogpost", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="If private, only these organizations may see the blog post.", + to="judge.Organization", + verbose_name="organizations", + ), ), ] diff --git a/judge/migrations/0115_auto_20210525_0222.py b/judge/migrations/0115_auto_20210525_0222.py index 91312de..52490c2 100644 --- a/judge/migrations/0115_auto_20210525_0222.py +++ b/judge/migrations/0115_auto_20210525_0222.py @@ -2,62 +2,109 @@ 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') + 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) + 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'), + ("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'}, + 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', + model_name="contest", + name="hide_scoreboard", ), migrations.RemoveField( - model_name='contest', - name='organizers', + 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'), + 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'), + 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'), + 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'), + 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'), + 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'), + model_name="contestparticipation", + name="tiebreaker", + field=models.FloatField(default=0.0, verbose_name="tie-breaking field"), ), ] diff --git a/judge/migrations/0116_auto_20211011_0645.py b/judge/migrations/0116_auto_20211011_0645.py index c067a59..79eaa8d 100644 --- a/judge/migrations/0116_auto_20211011_0645.py +++ b/judge/migrations/0116_auto_20211011_0645.py @@ -6,13 +6,25 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0115_auto_20210525_0222'), + ("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'), + 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", + ), ), ] diff --git a/judge/migrations/0117_auto_20211209_0612.py b/judge/migrations/0117_auto_20211209_0612.py index dd3879a..88c170f 100644 --- a/judge/migrations/0117_auto_20211209_0612.py +++ b/judge/migrations/0117_auto_20211209_0612.py @@ -6,13 +6,671 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0116_auto_20211011_0645'), + ("judge", "0116_auto_20211011_0645"), ] operations = [ migrations.AlterField( - model_name='profile', - name='timezone', - field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Nuuk', 'Nuuk'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kanton', 'Kanton'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='Asia/Ho_Chi_Minh', max_length=50, verbose_name='location'), + model_name="profile", + name="timezone", + field=models.CharField( + choices=[ + ( + "Africa", + [ + ("Africa/Abidjan", "Abidjan"), + ("Africa/Accra", "Accra"), + ("Africa/Addis_Ababa", "Addis_Ababa"), + ("Africa/Algiers", "Algiers"), + ("Africa/Asmara", "Asmara"), + ("Africa/Asmera", "Asmera"), + ("Africa/Bamako", "Bamako"), + ("Africa/Bangui", "Bangui"), + ("Africa/Banjul", "Banjul"), + ("Africa/Bissau", "Bissau"), + ("Africa/Blantyre", "Blantyre"), + ("Africa/Brazzaville", "Brazzaville"), + ("Africa/Bujumbura", "Bujumbura"), + ("Africa/Cairo", "Cairo"), + ("Africa/Casablanca", "Casablanca"), + ("Africa/Ceuta", "Ceuta"), + ("Africa/Conakry", "Conakry"), + ("Africa/Dakar", "Dakar"), + ("Africa/Dar_es_Salaam", "Dar_es_Salaam"), + ("Africa/Djibouti", "Djibouti"), + ("Africa/Douala", "Douala"), + ("Africa/El_Aaiun", "El_Aaiun"), + ("Africa/Freetown", "Freetown"), + ("Africa/Gaborone", "Gaborone"), + ("Africa/Harare", "Harare"), + ("Africa/Johannesburg", "Johannesburg"), + ("Africa/Juba", "Juba"), + ("Africa/Kampala", "Kampala"), + ("Africa/Khartoum", "Khartoum"), + ("Africa/Kigali", "Kigali"), + ("Africa/Kinshasa", "Kinshasa"), + ("Africa/Lagos", "Lagos"), + ("Africa/Libreville", "Libreville"), + ("Africa/Lome", "Lome"), + ("Africa/Luanda", "Luanda"), + ("Africa/Lubumbashi", "Lubumbashi"), + ("Africa/Lusaka", "Lusaka"), + ("Africa/Malabo", "Malabo"), + ("Africa/Maputo", "Maputo"), + ("Africa/Maseru", "Maseru"), + ("Africa/Mbabane", "Mbabane"), + ("Africa/Mogadishu", "Mogadishu"), + ("Africa/Monrovia", "Monrovia"), + ("Africa/Nairobi", "Nairobi"), + ("Africa/Ndjamena", "Ndjamena"), + ("Africa/Niamey", "Niamey"), + ("Africa/Nouakchott", "Nouakchott"), + ("Africa/Ouagadougou", "Ouagadougou"), + ("Africa/Porto-Novo", "Porto-Novo"), + ("Africa/Sao_Tome", "Sao_Tome"), + ("Africa/Timbuktu", "Timbuktu"), + ("Africa/Tripoli", "Tripoli"), + ("Africa/Tunis", "Tunis"), + ("Africa/Windhoek", "Windhoek"), + ], + ), + ( + "America", + [ + ("America/Adak", "Adak"), + ("America/Anchorage", "Anchorage"), + ("America/Anguilla", "Anguilla"), + ("America/Antigua", "Antigua"), + ("America/Araguaina", "Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "Argentina/Cordoba"), + ("America/Argentina/Jujuy", "Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "Argentina/Salta"), + ("America/Argentina/San_Juan", "Argentina/San_Juan"), + ("America/Argentina/San_Luis", "Argentina/San_Luis"), + ("America/Argentina/Tucuman", "Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "Argentina/Ushuaia"), + ("America/Aruba", "Aruba"), + ("America/Asuncion", "Asuncion"), + ("America/Atikokan", "Atikokan"), + ("America/Atka", "Atka"), + ("America/Bahia", "Bahia"), + ("America/Bahia_Banderas", "Bahia_Banderas"), + ("America/Barbados", "Barbados"), + ("America/Belem", "Belem"), + ("America/Belize", "Belize"), + ("America/Blanc-Sablon", "Blanc-Sablon"), + ("America/Boa_Vista", "Boa_Vista"), + ("America/Bogota", "Bogota"), + ("America/Boise", "Boise"), + ("America/Buenos_Aires", "Buenos_Aires"), + ("America/Cambridge_Bay", "Cambridge_Bay"), + ("America/Campo_Grande", "Campo_Grande"), + ("America/Cancun", "Cancun"), + ("America/Caracas", "Caracas"), + ("America/Catamarca", "Catamarca"), + ("America/Cayenne", "Cayenne"), + ("America/Cayman", "Cayman"), + ("America/Chicago", "Chicago"), + ("America/Chihuahua", "Chihuahua"), + ("America/Coral_Harbour", "Coral_Harbour"), + ("America/Cordoba", "Cordoba"), + ("America/Costa_Rica", "Costa_Rica"), + ("America/Creston", "Creston"), + ("America/Cuiaba", "Cuiaba"), + ("America/Curacao", "Curacao"), + ("America/Danmarkshavn", "Danmarkshavn"), + ("America/Dawson", "Dawson"), + ("America/Dawson_Creek", "Dawson_Creek"), + ("America/Denver", "Denver"), + ("America/Detroit", "Detroit"), + ("America/Dominica", "Dominica"), + ("America/Edmonton", "Edmonton"), + ("America/Eirunepe", "Eirunepe"), + ("America/El_Salvador", "El_Salvador"), + ("America/Ensenada", "Ensenada"), + ("America/Fort_Nelson", "Fort_Nelson"), + ("America/Fort_Wayne", "Fort_Wayne"), + ("America/Fortaleza", "Fortaleza"), + ("America/Glace_Bay", "Glace_Bay"), + ("America/Godthab", "Godthab"), + ("America/Goose_Bay", "Goose_Bay"), + ("America/Grand_Turk", "Grand_Turk"), + ("America/Grenada", "Grenada"), + ("America/Guadeloupe", "Guadeloupe"), + ("America/Guatemala", "Guatemala"), + ("America/Guayaquil", "Guayaquil"), + ("America/Guyana", "Guyana"), + ("America/Halifax", "Halifax"), + ("America/Havana", "Havana"), + ("America/Hermosillo", "Hermosillo"), + ("America/Indiana/Indianapolis", "Indiana/Indianapolis"), + ("America/Indiana/Knox", "Indiana/Knox"), + ("America/Indiana/Marengo", "Indiana/Marengo"), + ("America/Indiana/Petersburg", "Indiana/Petersburg"), + ("America/Indiana/Tell_City", "Indiana/Tell_City"), + ("America/Indiana/Vevay", "Indiana/Vevay"), + ("America/Indiana/Vincennes", "Indiana/Vincennes"), + ("America/Indiana/Winamac", "Indiana/Winamac"), + ("America/Indianapolis", "Indianapolis"), + ("America/Inuvik", "Inuvik"), + ("America/Iqaluit", "Iqaluit"), + ("America/Jamaica", "Jamaica"), + ("America/Jujuy", "Jujuy"), + ("America/Juneau", "Juneau"), + ("America/Kentucky/Louisville", "Kentucky/Louisville"), + ("America/Kentucky/Monticello", "Kentucky/Monticello"), + ("America/Knox_IN", "Knox_IN"), + ("America/Kralendijk", "Kralendijk"), + ("America/La_Paz", "La_Paz"), + ("America/Lima", "Lima"), + ("America/Los_Angeles", "Los_Angeles"), + ("America/Louisville", "Louisville"), + ("America/Lower_Princes", "Lower_Princes"), + ("America/Maceio", "Maceio"), + ("America/Managua", "Managua"), + ("America/Manaus", "Manaus"), + ("America/Marigot", "Marigot"), + ("America/Martinique", "Martinique"), + ("America/Matamoros", "Matamoros"), + ("America/Mazatlan", "Mazatlan"), + ("America/Mendoza", "Mendoza"), + ("America/Menominee", "Menominee"), + ("America/Merida", "Merida"), + ("America/Metlakatla", "Metlakatla"), + ("America/Mexico_City", "Mexico_City"), + ("America/Miquelon", "Miquelon"), + ("America/Moncton", "Moncton"), + ("America/Monterrey", "Monterrey"), + ("America/Montevideo", "Montevideo"), + ("America/Montreal", "Montreal"), + ("America/Montserrat", "Montserrat"), + ("America/Nassau", "Nassau"), + ("America/New_York", "New_York"), + ("America/Nipigon", "Nipigon"), + ("America/Nome", "Nome"), + ("America/Noronha", "Noronha"), + ("America/North_Dakota/Beulah", "North_Dakota/Beulah"), + ("America/North_Dakota/Center", "North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "North_Dakota/New_Salem", + ), + ("America/Nuuk", "Nuuk"), + ("America/Ojinaga", "Ojinaga"), + ("America/Panama", "Panama"), + ("America/Pangnirtung", "Pangnirtung"), + ("America/Paramaribo", "Paramaribo"), + ("America/Phoenix", "Phoenix"), + ("America/Port-au-Prince", "Port-au-Prince"), + ("America/Port_of_Spain", "Port_of_Spain"), + ("America/Porto_Acre", "Porto_Acre"), + ("America/Porto_Velho", "Porto_Velho"), + ("America/Puerto_Rico", "Puerto_Rico"), + ("America/Punta_Arenas", "Punta_Arenas"), + ("America/Rainy_River", "Rainy_River"), + ("America/Rankin_Inlet", "Rankin_Inlet"), + ("America/Recife", "Recife"), + ("America/Regina", "Regina"), + ("America/Resolute", "Resolute"), + ("America/Rio_Branco", "Rio_Branco"), + ("America/Rosario", "Rosario"), + ("America/Santa_Isabel", "Santa_Isabel"), + ("America/Santarem", "Santarem"), + ("America/Santiago", "Santiago"), + ("America/Santo_Domingo", "Santo_Domingo"), + ("America/Sao_Paulo", "Sao_Paulo"), + ("America/Scoresbysund", "Scoresbysund"), + ("America/Shiprock", "Shiprock"), + ("America/Sitka", "Sitka"), + ("America/St_Barthelemy", "St_Barthelemy"), + ("America/St_Johns", "St_Johns"), + ("America/St_Kitts", "St_Kitts"), + ("America/St_Lucia", "St_Lucia"), + ("America/St_Thomas", "St_Thomas"), + ("America/St_Vincent", "St_Vincent"), + ("America/Swift_Current", "Swift_Current"), + ("America/Tegucigalpa", "Tegucigalpa"), + ("America/Thule", "Thule"), + ("America/Thunder_Bay", "Thunder_Bay"), + ("America/Tijuana", "Tijuana"), + ("America/Toronto", "Toronto"), + ("America/Tortola", "Tortola"), + ("America/Vancouver", "Vancouver"), + ("America/Virgin", "Virgin"), + ("America/Whitehorse", "Whitehorse"), + ("America/Winnipeg", "Winnipeg"), + ("America/Yakutat", "Yakutat"), + ("America/Yellowknife", "Yellowknife"), + ], + ), + ( + "Antarctica", + [ + ("Antarctica/Casey", "Casey"), + ("Antarctica/Davis", "Davis"), + ("Antarctica/DumontDUrville", "DumontDUrville"), + ("Antarctica/Macquarie", "Macquarie"), + ("Antarctica/Mawson", "Mawson"), + ("Antarctica/McMurdo", "McMurdo"), + ("Antarctica/Palmer", "Palmer"), + ("Antarctica/Rothera", "Rothera"), + ("Antarctica/South_Pole", "South_Pole"), + ("Antarctica/Syowa", "Syowa"), + ("Antarctica/Troll", "Troll"), + ("Antarctica/Vostok", "Vostok"), + ], + ), + ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]), + ( + "Asia", + [ + ("Asia/Aden", "Aden"), + ("Asia/Almaty", "Almaty"), + ("Asia/Amman", "Amman"), + ("Asia/Anadyr", "Anadyr"), + ("Asia/Aqtau", "Aqtau"), + ("Asia/Aqtobe", "Aqtobe"), + ("Asia/Ashgabat", "Ashgabat"), + ("Asia/Ashkhabad", "Ashkhabad"), + ("Asia/Atyrau", "Atyrau"), + ("Asia/Baghdad", "Baghdad"), + ("Asia/Bahrain", "Bahrain"), + ("Asia/Baku", "Baku"), + ("Asia/Bangkok", "Bangkok"), + ("Asia/Barnaul", "Barnaul"), + ("Asia/Beirut", "Beirut"), + ("Asia/Bishkek", "Bishkek"), + ("Asia/Brunei", "Brunei"), + ("Asia/Calcutta", "Calcutta"), + ("Asia/Chita", "Chita"), + ("Asia/Choibalsan", "Choibalsan"), + ("Asia/Chongqing", "Chongqing"), + ("Asia/Chungking", "Chungking"), + ("Asia/Colombo", "Colombo"), + ("Asia/Dacca", "Dacca"), + ("Asia/Damascus", "Damascus"), + ("Asia/Dhaka", "Dhaka"), + ("Asia/Dili", "Dili"), + ("Asia/Dubai", "Dubai"), + ("Asia/Dushanbe", "Dushanbe"), + ("Asia/Famagusta", "Famagusta"), + ("Asia/Gaza", "Gaza"), + ("Asia/Harbin", "Harbin"), + ("Asia/Hebron", "Hebron"), + ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Hong_Kong"), + ("Asia/Hovd", "Hovd"), + ("Asia/Irkutsk", "Irkutsk"), + ("Asia/Istanbul", "Istanbul"), + ("Asia/Jakarta", "Jakarta"), + ("Asia/Jayapura", "Jayapura"), + ("Asia/Jerusalem", "Jerusalem"), + ("Asia/Kabul", "Kabul"), + ("Asia/Kamchatka", "Kamchatka"), + ("Asia/Karachi", "Karachi"), + ("Asia/Kashgar", "Kashgar"), + ("Asia/Kathmandu", "Kathmandu"), + ("Asia/Katmandu", "Katmandu"), + ("Asia/Khandyga", "Khandyga"), + ("Asia/Kolkata", "Kolkata"), + ("Asia/Krasnoyarsk", "Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Kuala_Lumpur"), + ("Asia/Kuching", "Kuching"), + ("Asia/Kuwait", "Kuwait"), + ("Asia/Macao", "Macao"), + ("Asia/Macau", "Macau"), + ("Asia/Magadan", "Magadan"), + ("Asia/Makassar", "Makassar"), + ("Asia/Manila", "Manila"), + ("Asia/Muscat", "Muscat"), + ("Asia/Nicosia", "Nicosia"), + ("Asia/Novokuznetsk", "Novokuznetsk"), + ("Asia/Novosibirsk", "Novosibirsk"), + ("Asia/Omsk", "Omsk"), + ("Asia/Oral", "Oral"), + ("Asia/Phnom_Penh", "Phnom_Penh"), + ("Asia/Pontianak", "Pontianak"), + ("Asia/Pyongyang", "Pyongyang"), + ("Asia/Qatar", "Qatar"), + ("Asia/Qostanay", "Qostanay"), + ("Asia/Qyzylorda", "Qyzylorda"), + ("Asia/Rangoon", "Rangoon"), + ("Asia/Riyadh", "Riyadh"), + ("Asia/Saigon", "Saigon"), + ("Asia/Sakhalin", "Sakhalin"), + ("Asia/Samarkand", "Samarkand"), + ("Asia/Seoul", "Seoul"), + ("Asia/Shanghai", "Shanghai"), + ("Asia/Singapore", "Singapore"), + ("Asia/Srednekolymsk", "Srednekolymsk"), + ("Asia/Taipei", "Taipei"), + ("Asia/Tashkent", "Tashkent"), + ("Asia/Tbilisi", "Tbilisi"), + ("Asia/Tehran", "Tehran"), + ("Asia/Tel_Aviv", "Tel_Aviv"), + ("Asia/Thimbu", "Thimbu"), + ("Asia/Thimphu", "Thimphu"), + ("Asia/Tokyo", "Tokyo"), + ("Asia/Tomsk", "Tomsk"), + ("Asia/Ujung_Pandang", "Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Ulaanbaatar"), + ("Asia/Ulan_Bator", "Ulan_Bator"), + ("Asia/Urumqi", "Urumqi"), + ("Asia/Ust-Nera", "Ust-Nera"), + ("Asia/Vientiane", "Vientiane"), + ("Asia/Vladivostok", "Vladivostok"), + ("Asia/Yakutsk", "Yakutsk"), + ("Asia/Yangon", "Yangon"), + ("Asia/Yekaterinburg", "Yekaterinburg"), + ("Asia/Yerevan", "Yerevan"), + ], + ), + ( + "Atlantic", + [ + ("Atlantic/Azores", "Azores"), + ("Atlantic/Bermuda", "Bermuda"), + ("Atlantic/Canary", "Canary"), + ("Atlantic/Cape_Verde", "Cape_Verde"), + ("Atlantic/Faeroe", "Faeroe"), + ("Atlantic/Faroe", "Faroe"), + ("Atlantic/Jan_Mayen", "Jan_Mayen"), + ("Atlantic/Madeira", "Madeira"), + ("Atlantic/Reykjavik", "Reykjavik"), + ("Atlantic/South_Georgia", "South_Georgia"), + ("Atlantic/St_Helena", "St_Helena"), + ("Atlantic/Stanley", "Stanley"), + ], + ), + ( + "Australia", + [ + ("Australia/ACT", "ACT"), + ("Australia/Adelaide", "Adelaide"), + ("Australia/Brisbane", "Brisbane"), + ("Australia/Broken_Hill", "Broken_Hill"), + ("Australia/Canberra", "Canberra"), + ("Australia/Currie", "Currie"), + ("Australia/Darwin", "Darwin"), + ("Australia/Eucla", "Eucla"), + ("Australia/Hobart", "Hobart"), + ("Australia/LHI", "LHI"), + ("Australia/Lindeman", "Lindeman"), + ("Australia/Lord_Howe", "Lord_Howe"), + ("Australia/Melbourne", "Melbourne"), + ("Australia/NSW", "NSW"), + ("Australia/North", "North"), + ("Australia/Perth", "Perth"), + ("Australia/Queensland", "Queensland"), + ("Australia/South", "South"), + ("Australia/Sydney", "Sydney"), + ("Australia/Tasmania", "Tasmania"), + ("Australia/Victoria", "Victoria"), + ("Australia/West", "West"), + ("Australia/Yancowinna", "Yancowinna"), + ], + ), + ( + "Brazil", + [ + ("Brazil/Acre", "Acre"), + ("Brazil/DeNoronha", "DeNoronha"), + ("Brazil/East", "East"), + ("Brazil/West", "West"), + ], + ), + ( + "Canada", + [ + ("Canada/Atlantic", "Atlantic"), + ("Canada/Central", "Central"), + ("Canada/Eastern", "Eastern"), + ("Canada/Mountain", "Mountain"), + ("Canada/Newfoundland", "Newfoundland"), + ("Canada/Pacific", "Pacific"), + ("Canada/Saskatchewan", "Saskatchewan"), + ("Canada/Yukon", "Yukon"), + ], + ), + ( + "Chile", + [ + ("Chile/Continental", "Continental"), + ("Chile/EasterIsland", "EasterIsland"), + ], + ), + ( + "Etc", + [ + ("Etc/Greenwich", "Greenwich"), + ("Etc/UCT", "UCT"), + ("Etc/UTC", "UTC"), + ("Etc/Universal", "Universal"), + ("Etc/Zulu", "Zulu"), + ], + ), + ( + "Europe", + [ + ("Europe/Amsterdam", "Amsterdam"), + ("Europe/Andorra", "Andorra"), + ("Europe/Astrakhan", "Astrakhan"), + ("Europe/Athens", "Athens"), + ("Europe/Belfast", "Belfast"), + ("Europe/Belgrade", "Belgrade"), + ("Europe/Berlin", "Berlin"), + ("Europe/Bratislava", "Bratislava"), + ("Europe/Brussels", "Brussels"), + ("Europe/Bucharest", "Bucharest"), + ("Europe/Budapest", "Budapest"), + ("Europe/Busingen", "Busingen"), + ("Europe/Chisinau", "Chisinau"), + ("Europe/Copenhagen", "Copenhagen"), + ("Europe/Dublin", "Dublin"), + ("Europe/Gibraltar", "Gibraltar"), + ("Europe/Guernsey", "Guernsey"), + ("Europe/Helsinki", "Helsinki"), + ("Europe/Isle_of_Man", "Isle_of_Man"), + ("Europe/Istanbul", "Istanbul"), + ("Europe/Jersey", "Jersey"), + ("Europe/Kaliningrad", "Kaliningrad"), + ("Europe/Kiev", "Kiev"), + ("Europe/Kirov", "Kirov"), + ("Europe/Lisbon", "Lisbon"), + ("Europe/Ljubljana", "Ljubljana"), + ("Europe/London", "London"), + ("Europe/Luxembourg", "Luxembourg"), + ("Europe/Madrid", "Madrid"), + ("Europe/Malta", "Malta"), + ("Europe/Mariehamn", "Mariehamn"), + ("Europe/Minsk", "Minsk"), + ("Europe/Monaco", "Monaco"), + ("Europe/Moscow", "Moscow"), + ("Europe/Nicosia", "Nicosia"), + ("Europe/Oslo", "Oslo"), + ("Europe/Paris", "Paris"), + ("Europe/Podgorica", "Podgorica"), + ("Europe/Prague", "Prague"), + ("Europe/Riga", "Riga"), + ("Europe/Rome", "Rome"), + ("Europe/Samara", "Samara"), + ("Europe/San_Marino", "San_Marino"), + ("Europe/Sarajevo", "Sarajevo"), + ("Europe/Saratov", "Saratov"), + ("Europe/Simferopol", "Simferopol"), + ("Europe/Skopje", "Skopje"), + ("Europe/Sofia", "Sofia"), + ("Europe/Stockholm", "Stockholm"), + ("Europe/Tallinn", "Tallinn"), + ("Europe/Tirane", "Tirane"), + ("Europe/Tiraspol", "Tiraspol"), + ("Europe/Ulyanovsk", "Ulyanovsk"), + ("Europe/Uzhgorod", "Uzhgorod"), + ("Europe/Vaduz", "Vaduz"), + ("Europe/Vatican", "Vatican"), + ("Europe/Vienna", "Vienna"), + ("Europe/Vilnius", "Vilnius"), + ("Europe/Volgograd", "Volgograd"), + ("Europe/Warsaw", "Warsaw"), + ("Europe/Zagreb", "Zagreb"), + ("Europe/Zaporozhye", "Zaporozhye"), + ("Europe/Zurich", "Zurich"), + ], + ), + ( + "Indian", + [ + ("Indian/Antananarivo", "Antananarivo"), + ("Indian/Chagos", "Chagos"), + ("Indian/Christmas", "Christmas"), + ("Indian/Cocos", "Cocos"), + ("Indian/Comoro", "Comoro"), + ("Indian/Kerguelen", "Kerguelen"), + ("Indian/Mahe", "Mahe"), + ("Indian/Maldives", "Maldives"), + ("Indian/Mauritius", "Mauritius"), + ("Indian/Mayotte", "Mayotte"), + ("Indian/Reunion", "Reunion"), + ], + ), + ( + "Mexico", + [ + ("Mexico/BajaNorte", "BajaNorte"), + ("Mexico/BajaSur", "BajaSur"), + ("Mexico/General", "General"), + ], + ), + ( + "Other", + [ + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + ), + ( + "Pacific", + [ + ("Pacific/Apia", "Apia"), + ("Pacific/Auckland", "Auckland"), + ("Pacific/Bougainville", "Bougainville"), + ("Pacific/Chatham", "Chatham"), + ("Pacific/Chuuk", "Chuuk"), + ("Pacific/Easter", "Easter"), + ("Pacific/Efate", "Efate"), + ("Pacific/Enderbury", "Enderbury"), + ("Pacific/Fakaofo", "Fakaofo"), + ("Pacific/Fiji", "Fiji"), + ("Pacific/Funafuti", "Funafuti"), + ("Pacific/Galapagos", "Galapagos"), + ("Pacific/Gambier", "Gambier"), + ("Pacific/Guadalcanal", "Guadalcanal"), + ("Pacific/Guam", "Guam"), + ("Pacific/Honolulu", "Honolulu"), + ("Pacific/Johnston", "Johnston"), + ("Pacific/Kanton", "Kanton"), + ("Pacific/Kiritimati", "Kiritimati"), + ("Pacific/Kosrae", "Kosrae"), + ("Pacific/Kwajalein", "Kwajalein"), + ("Pacific/Majuro", "Majuro"), + ("Pacific/Marquesas", "Marquesas"), + ("Pacific/Midway", "Midway"), + ("Pacific/Nauru", "Nauru"), + ("Pacific/Niue", "Niue"), + ("Pacific/Norfolk", "Norfolk"), + ("Pacific/Noumea", "Noumea"), + ("Pacific/Pago_Pago", "Pago_Pago"), + ("Pacific/Palau", "Palau"), + ("Pacific/Pitcairn", "Pitcairn"), + ("Pacific/Pohnpei", "Pohnpei"), + ("Pacific/Ponape", "Ponape"), + ("Pacific/Port_Moresby", "Port_Moresby"), + ("Pacific/Rarotonga", "Rarotonga"), + ("Pacific/Saipan", "Saipan"), + ("Pacific/Samoa", "Samoa"), + ("Pacific/Tahiti", "Tahiti"), + ("Pacific/Tarawa", "Tarawa"), + ("Pacific/Tongatapu", "Tongatapu"), + ("Pacific/Truk", "Truk"), + ("Pacific/Wake", "Wake"), + ("Pacific/Wallis", "Wallis"), + ("Pacific/Yap", "Yap"), + ], + ), + ( + "US", + [ + ("US/Alaska", "Alaska"), + ("US/Aleutian", "Aleutian"), + ("US/Arizona", "Arizona"), + ("US/Central", "Central"), + ("US/East-Indiana", "East-Indiana"), + ("US/Eastern", "Eastern"), + ("US/Hawaii", "Hawaii"), + ("US/Indiana-Starke", "Indiana-Starke"), + ("US/Michigan", "Michigan"), + ("US/Mountain", "Mountain"), + ("US/Pacific", "Pacific"), + ("US/Samoa", "Samoa"), + ], + ), + ], + default="Asia/Ho_Chi_Minh", + max_length=50, + verbose_name="location", + ), ), ] diff --git a/judge/migrations/0118_rating.py b/judge/migrations/0118_rating.py index 8ad6ad2..fb62906 100644 --- a/judge/migrations/0118_rating.py +++ b/judge/migrations/0118_rating.py @@ -7,7 +7,7 @@ from django.db.models.functions import Coalesce from django.utils import timezone -def tie_ranker(iterable, key=attrgetter('points')): +def tie_ranker(iterable, key=attrgetter("points")): rank = 0 delta = 1 last = None @@ -53,7 +53,9 @@ 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): +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) @@ -74,7 +76,9 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is 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]) + 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) @@ -98,8 +102,10 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is 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)) + 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 @@ -112,23 +118,49 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is # 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: + 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))) + 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') + 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: @@ -137,46 +169,68 @@ def tc_rate_contest(contest, Rating, Profile): 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) + 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)] + 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])) + 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') + 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) + 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'): + 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') + 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) + cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table) Profile.objects.update(rating=None) # Don't populate Rating @@ -184,25 +238,25 @@ def rate_elo_mmr(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('judge', '0117_auto_20211209_0612'), + ("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'), + 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'), + 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'), + model_name="rating", + name="volatility", + field=models.IntegerField(verbose_name="volatility"), ), migrations.RunPython(rate_elo_mmr, migrations.RunPython.noop, atomic=True), - ] \ No newline at end of file + ] diff --git a/judge/migrations/0119_auto_20220306_0512.py b/judge/migrations/0119_auto_20220306_0512.py index c85f792..abcf1fb 100644 --- a/judge/migrations/0119_auto_20220306_0512.py +++ b/judge/migrations/0119_auto_20220306_0512.py @@ -6,13 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0118_rating'), + ("judge", "0118_rating"), ] operations = [ migrations.AlterField( - model_name='contest', - name='hide_problem_tags', - field=models.BooleanField(default=True, help_text='Whether problem tags should be hidden by default.', verbose_name='hide problem tags'), + model_name="contest", + name="hide_problem_tags", + field=models.BooleanField( + default=True, + help_text="Whether problem tags should be hidden by default.", + verbose_name="hide problem tags", + ), ), ] diff --git a/judge/migrations/0120_auto_20220306_1124.py b/judge/migrations/0120_auto_20220306_1124.py index 849461c..5e02189 100644 --- a/judge/migrations/0120_auto_20220306_1124.py +++ b/judge/migrations/0120_auto_20220306_1124.py @@ -8,27 +8,68 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0119_auto_20220306_0512'), + ("judge", "0119_auto_20220306_0512"), ] operations = [ migrations.AddField( - model_name='profile', - name='is_banned_problem_voting', - field=models.BooleanField(default=False, help_text="User will not be able to vote on problems' point values.", verbose_name='banned from voting'), + model_name="profile", + name="is_banned_problem_voting", + field=models.BooleanField( + default=False, + help_text="User will not be able to vote on problems' point values.", + verbose_name="banned from voting", + ), ), migrations.CreateModel( - name='ProblemPointsVote', + name="ProblemPointsVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('points', models.IntegerField(help_text='The amount of points you think this problem deserves.', validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(600)], verbose_name='proposed point value')), - ('vote_time', models.DateTimeField(auto_now_add=True, verbose_name='The time this vote was cast')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Problem')), - ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "points", + models.IntegerField( + help_text="The amount of points you think this problem deserves.", + validators=[ + django.core.validators.MinValueValidator(100), + django.core.validators.MaxValueValidator(600), + ], + verbose_name="proposed point value", + ), + ), + ( + "vote_time", + models.DateTimeField( + auto_now_add=True, verbose_name="The time this vote was cast" + ), + ), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="problem_points_votes", + to="judge.Problem", + ), + ), + ( + "voter", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="problem_points_votes", + to="judge.Profile", + ), + ), ], options={ - 'verbose_name': 'vote', - 'verbose_name_plural': 'votes', + "verbose_name": "vote", + "verbose_name_plural": "votes", }, ), ] diff --git a/judge/migrations/0121_auto_20220415_0135.py b/judge/migrations/0121_auto_20220415_0135.py index bc70618..77d3f6a 100644 --- a/judge/migrations/0121_auto_20220415_0135.py +++ b/judge/migrations/0121_auto_20220415_0135.py @@ -9,23 +9,68 @@ import judge.utils.problem_data class Migration(migrations.Migration): dependencies = [ - ('judge', '0120_auto_20220306_1124'), + ("judge", "0120_auto_20220306_1124"), ] operations = [ migrations.AddField( - model_name='problemdata', - name='interactive_judge', - field=models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['cpp'])], verbose_name='interactive judge'), + model_name="problemdata", + name="interactive_judge", + field=models.FileField( + blank=True, + null=True, + storage=judge.utils.problem_data.ProblemDataStorage(), + upload_to=judge.models.problem_data.problem_directory_file, + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["cpp"] + ) + ], + verbose_name="interactive judge", + ), ), migrations.AlterField( - model_name='problemdata', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)'), ('interact', 'Interactive')], max_length=10, verbose_name='checker'), + model_name="problemdata", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ("interact", "Interactive"), + ], + max_length=10, + verbose_name="checker", + ), ), migrations.AlterField( - model_name='problemtestcase', - name='checker', - field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line'), ('custom', 'Custom checker (PY)'), ('customval', 'Custom validator (CPP)'), ('interact', 'Interactive')], max_length=10, verbose_name='checker'), + model_name="problemtestcase", + name="checker", + field=models.CharField( + blank=True, + choices=[ + ("standard", "Standard"), + ("floats", "Floats"), + ("floatsabs", "Floats (absolute)"), + ("floatsrel", "Floats (relative)"), + ("rstripped", "Non-trailing spaces"), + ("sorted", "Unordered"), + ("identical", "Byte identical"), + ("linecount", "Line-by-line"), + ("custom", "Custom checker (PY)"), + ("customval", "Custom validator (CPP)"), + ("interact", "Interactive"), + ], + max_length=10, + verbose_name="checker", + ), ), ] diff --git a/judge/migrations/0122_auto_20220425_1202.py b/judge/migrations/0122_auto_20220425_1202.py index ee48db3..427cd98 100644 --- a/judge/migrations/0122_auto_20220425_1202.py +++ b/judge/migrations/0122_auto_20220425_1202.py @@ -6,13 +6,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('judge', '0121_auto_20220415_0135'), + ("judge", "0121_auto_20220415_0135"), ] operations = [ migrations.AlterField( - model_name='contest', - name='time_limit', - field=models.DurationField(blank=True, help_text='Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00', null=True, verbose_name='time limit'), + model_name="contest", + name="time_limit", + field=models.DurationField( + blank=True, + help_text="Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00", + null=True, + verbose_name="time limit", + ), ), ] diff --git a/judge/migrations/0123_auto_20220502_2356.py b/judge/migrations/0123_auto_20220502_2356.py index f681a51..54ed0c7 100644 --- a/judge/migrations/0123_auto_20220502_2356.py +++ b/judge/migrations/0123_auto_20220502_2356.py @@ -7,30 +7,85 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('judge', '0122_auto_20220425_1202'), + ("judge", "0122_auto_20220425_1202"), ] operations = [ migrations.AlterModelOptions( - name='problem', - options={'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems'), ('suggest_problem_changes', 'Suggest changes to problem')), 'verbose_name': 'problem', 'verbose_name_plural': 'problems'}, + name="problem", + options={ + "permissions": ( + ("see_private_problem", "See hidden problems"), + ("edit_own_problem", "Edit own problems"), + ("edit_all_problem", "Edit all problems"), + ("edit_public_problem", "Edit all public problems"), + ("clone_problem", "Clone problem"), + ("change_public_visibility", "Change is_public field"), + ("change_manually_managed", "Change is_manually_managed field"), + ("see_organization_problem", "See organization-private problems"), + ("suggest_problem_changes", "Suggest changes to problem"), + ), + "verbose_name": "problem", + "verbose_name_plural": "problems", + }, ), migrations.CreateModel( - name='VolunteerProblemVote', + name="VolunteerProblemVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True)), - ('knowledge_points', models.PositiveIntegerField(help_text='Points awarded by knowledge difficulty', verbose_name='knowledge points')), - ('thinking_points', models.PositiveIntegerField(help_text='Points awarded by thinking difficulty', verbose_name='thinking points')), - ('feedback', models.TextField(blank=True, verbose_name='feedback')), - ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_user_votes', to='judge.Problem')), - ('types', models.ManyToManyField(help_text="The type of problem, as shown on the problem's page.", to='judge.ProblemType', verbose_name='problem types')), - ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_problem_votes', to='judge.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time", models.DateTimeField(auto_now_add=True)), + ( + "knowledge_points", + models.PositiveIntegerField( + help_text="Points awarded by knowledge difficulty", + verbose_name="knowledge points", + ), + ), + ( + "thinking_points", + models.PositiveIntegerField( + help_text="Points awarded by thinking difficulty", + verbose_name="thinking points", + ), + ), + ("feedback", models.TextField(blank=True, verbose_name="feedback")), + ( + "problem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="volunteer_user_votes", + to="judge.Problem", + ), + ), + ( + "types", + models.ManyToManyField( + help_text="The type of problem, as shown on the problem's page.", + to="judge.ProblemType", + verbose_name="problem types", + ), + ), + ( + "voter", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="volunteer_problem_votes", + to="judge.Profile", + ), + ), ], options={ - 'verbose_name': 'volunteer vote', - 'verbose_name_plural': 'volunteer votes', - 'unique_together': {('voter', 'problem')}, + "verbose_name": "volunteer vote", + "verbose_name_plural": "volunteer votes", + "unique_together": {("voter", "problem")}, }, ), ] diff --git a/judge/ml/collab_filter.py b/judge/ml/collab_filter.py index 2a8171f..3719377 100644 --- a/judge/ml/collab_filter.py +++ b/judge/ml/collab_filter.py @@ -5,14 +5,16 @@ from dmoj.decorators import timeit class CollabFilter: - DOT = 'dot' - COSINE = 'cosine' + DOT = "dot" + COSINE = "cosine" # name = 'collab_filter' or 'collab_filter_time' @timeit def __init__(self, name, **kwargs): - embeddings = np.load(os.path.join(settings.ML_OUTPUT_PATH, name + '/embeddings.npz'), - allow_pickle=True) + embeddings = np.load( + os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"), + allow_pickle=True, + ) arr0, arr1 = embeddings.files self.user_embeddings = embeddings[arr0] self.problem_embeddings = embeddings[arr1] @@ -42,9 +44,10 @@ class CollabFilter: if uid >= len(self.user_embeddings): uid = 0 scores = self.compute_scores( - self.user_embeddings[uid], self.problem_embeddings, measure) - - res = [] # [(score, problem)] + self.user_embeddings[uid], self.problem_embeddings, measure + ) + + res = [] # [(score, problem)] for problem in problems: pid = problem.id if pid < len(scores): @@ -53,17 +56,17 @@ class CollabFilter: res.sort(reverse=True, key=lambda x: x[0]) return res[:limit] - # return a list of pid def problems_neighbors(self, problem, problemset, measure=DOT, limit=None): pid = problem.id if pid >= len(self.problem_embeddings): return None scores = self.compute_scores( - self.problem_embeddings[pid], self.problem_embeddings, measure) + self.problem_embeddings[pid], self.problem_embeddings, measure + ) res = [] for p in problemset: if p.id < len(scores): res.append((scores[p.id], p)) res.sort(reverse=True, key=lambda x: x[0]) - return res[:limit] \ No newline at end of file + return res[:limit] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 88367c3..8eaf102 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -1,32 +1,67 @@ from reversion import revisions -from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE +from judge.models.choices import ( + ACE_THEMES, + EFFECTIVE_MATH_ENGINES, + MATH_ENGINES_CHOICES, + TIMEZONE, +) from judge.models.comment import Comment, CommentLock, CommentVote, Notification -from judge.models.contest import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestSubmission, \ - ContestTag, Rating +from judge.models.contest import ( + Contest, + ContestMoss, + ContestParticipation, + ContestProblem, + ContestSubmission, + ContestTag, + Rating, +) from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.message import PrivateMessage, PrivateMessageThread -from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ - ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet, ProblemPointsVote -from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ - problem_directory_file +from judge.models.problem import ( + LanguageLimit, + License, + Problem, + ProblemClarification, + ProblemGroup, + ProblemTranslation, + ProblemType, + Solution, + TranslatedProblemForeignKeyQuerySet, + TranslatedProblemQuerySet, + ProblemPointsVote, +) +from judge.models.problem_data import ( + CHECKERS, + ProblemData, + ProblemTestCase, + problem_data_storage, + problem_directory_file, +) from judge.models.profile import Organization, OrganizationRequest, Profile, Friend from judge.models.runtime import Judge, Language, RuntimeVersion -from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase +from judge.models.submission import ( + SUBMISSION_RESULT, + Submission, + SubmissionSource, + SubmissionTestCase, +) from judge.models.ticket import Ticket, TicketMessage from judge.models.volunteer import VolunteerProblemVote -revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating']) -revisions.register(Problem, follow=['language_limits']) +revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) +revisions.register(Problem, follow=["language_limits"]) revisions.register(LanguageLimit) -revisions.register(Contest, follow=['contest_problems']) +revisions.register(Contest, follow=["contest_problems"]) revisions.register(ContestProblem) revisions.register(Organization) revisions.register(BlogPost) revisions.register(Solution) -revisions.register(Judge, fields=['name', 'created', 'auth_key', 'description']) +revisions.register(Judge, fields=["name", "created", "auth_key", "description"]) revisions.register(Language) -revisions.register(Comment, fields=['author', 'time', 'page', 'score', 'body', 'hidden', 'parent']) +revisions.register( + Comment, fields=["author", "time", "page", "score", "body", "hidden", "parent"] +) revisions.register(ProblemTranslation) revisions.register(ProblemPointsVote) revisions.register(ContestMoss) diff --git a/judge/models/choices.py b/judge/models/choices.py index 16c57de..b6fab5c 100644 --- a/judge/models/choices.py +++ b/judge/models/choices.py @@ -8,11 +8,11 @@ from django.utils.translation import gettext_lazy as _ def make_timezones(): data = defaultdict(list) for tz in pytz.all_timezones: - if '/' in tz: - area, loc = tz.split('/', 1) + if "/" in tz: + area, loc = tz.split("/", 1) else: - area, loc = 'Other', tz - if not loc.startswith('GMT'): + area, loc = "Other", tz + if not loc.startswith("GMT"): data[area].append((tz, loc)) return sorted(data.items(), key=itemgetter(0)) @@ -21,46 +21,46 @@ TIMEZONE = make_timezones() del make_timezones ACE_THEMES = ( - ('ambiance', 'Ambiance'), - ('chaos', 'Chaos'), - ('chrome', 'Chrome'), - ('clouds', 'Clouds'), - ('clouds_midnight', 'Clouds Midnight'), - ('cobalt', 'Cobalt'), - ('crimson_editor', 'Crimson Editor'), - ('dawn', 'Dawn'), - ('dreamweaver', 'Dreamweaver'), - ('eclipse', 'Eclipse'), - ('github', 'Github'), - ('idle_fingers', 'Idle Fingers'), - ('katzenmilch', 'Katzenmilch'), - ('kr_theme', 'KR Theme'), - ('kuroir', 'Kuroir'), - ('merbivore', 'Merbivore'), - ('merbivore_soft', 'Merbivore Soft'), - ('mono_industrial', 'Mono Industrial'), - ('monokai', 'Monokai'), - ('pastel_on_dark', 'Pastel on Dark'), - ('solarized_dark', 'Solarized Dark'), - ('solarized_light', 'Solarized Light'), - ('terminal', 'Terminal'), - ('textmate', 'Textmate'), - ('tomorrow', 'Tomorrow'), - ('tomorrow_night', 'Tomorrow Night'), - ('tomorrow_night_blue', 'Tomorrow Night Blue'), - ('tomorrow_night_bright', 'Tomorrow Night Bright'), - ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), - ('twilight', 'Twilight'), - ('vibrant_ink', 'Vibrant Ink'), - ('xcode', 'XCode'), + ("ambiance", "Ambiance"), + ("chaos", "Chaos"), + ("chrome", "Chrome"), + ("clouds", "Clouds"), + ("clouds_midnight", "Clouds Midnight"), + ("cobalt", "Cobalt"), + ("crimson_editor", "Crimson Editor"), + ("dawn", "Dawn"), + ("dreamweaver", "Dreamweaver"), + ("eclipse", "Eclipse"), + ("github", "Github"), + ("idle_fingers", "Idle Fingers"), + ("katzenmilch", "Katzenmilch"), + ("kr_theme", "KR Theme"), + ("kuroir", "Kuroir"), + ("merbivore", "Merbivore"), + ("merbivore_soft", "Merbivore Soft"), + ("mono_industrial", "Mono Industrial"), + ("monokai", "Monokai"), + ("pastel_on_dark", "Pastel on Dark"), + ("solarized_dark", "Solarized Dark"), + ("solarized_light", "Solarized Light"), + ("terminal", "Terminal"), + ("textmate", "Textmate"), + ("tomorrow", "Tomorrow"), + ("tomorrow_night", "Tomorrow Night"), + ("tomorrow_night_blue", "Tomorrow Night Blue"), + ("tomorrow_night_bright", "Tomorrow Night Bright"), + ("tomorrow_night_eighties", "Tomorrow Night Eighties"), + ("twilight", "Twilight"), + ("vibrant_ink", "Vibrant Ink"), + ("xcode", "XCode"), ) MATH_ENGINES_CHOICES = ( - ('tex', _('Leave as LaTeX')), - ('svg', _('SVG with PNG fallback')), - ('mml', _('MathML only')), - ('jax', _('MathJax with SVG/PNG fallback')), - ('auto', _('Detect best quality')), + ("tex", _("Leave as LaTeX")), + ("svg", _("SVG with PNG fallback")), + ("mml", _("MathML only")), + ("jax", _("MathJax with SVG/PNG fallback")), + ("auto", _("Detect best quality")), ) -EFFECTIVE_MATH_ENGINES = ('svg', 'mml', 'tex', 'jax') +EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax") diff --git a/judge/models/comment.py b/judge/models/comment.py index 5d26500..a4ffc57 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -20,77 +20,98 @@ from judge.models.profile import Profile from judge.utils.cachedict import CacheDict -__all__ = ['Comment', 'CommentLock', 'CommentVote', 'Notification'] +__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"] -comment_validator = RegexValidator(r'^[pcs]:[a-z0-9]+$|^b:\d+$', - _(r'Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$')) +comment_validator = RegexValidator( + r"^[pcs]:[a-z0-9]+$|^b:\d+$", _(r"Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$") +) class VersionRelation(GenericRelation): def __init__(self): - super(VersionRelation, self).__init__(Version, object_id_field='object_id') + super(VersionRelation, self).__init__(Version, object_id_field="object_id") def get_extra_restriction(self, where_class, alias, remote_alias): - cond = super(VersionRelation, self).get_extra_restriction(where_class, alias, remote_alias) - field = self.remote_field.model._meta.get_field('db') - lookup = field.get_lookup('exact')(field.get_col(remote_alias), 'default') - cond.add(lookup, 'AND') + cond = super(VersionRelation, self).get_extra_restriction( + where_class, alias, remote_alias + ) + field = self.remote_field.model._meta.get_field("db") + lookup = field.get_lookup("exact")(field.get_col(remote_alias), "default") + cond.add(lookup, "AND") return cond class Comment(MPTTModel): - author = models.ForeignKey(Profile, verbose_name=_('commenter'), on_delete=CASCADE) - time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) - page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True, - validators=[comment_validator]) - score = models.IntegerField(verbose_name=_('votes'), default=0) - body = models.TextField(verbose_name=_('body of comment'), max_length=8192) - hidden = models.BooleanField(verbose_name=_('hide the comment'), default=0) - parent = TreeForeignKey('self', verbose_name=_('parent'), null=True, blank=True, related_name='replies', - on_delete=CASCADE) + author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE) + time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) + page = models.CharField( + max_length=30, + verbose_name=_("associated page"), + db_index=True, + validators=[comment_validator], + ) + score = models.IntegerField(verbose_name=_("votes"), default=0) + body = models.TextField(verbose_name=_("body of comment"), max_length=8192) + hidden = models.BooleanField(verbose_name=_("hide the comment"), default=0) + parent = TreeForeignKey( + "self", + verbose_name=_("parent"), + null=True, + blank=True, + related_name="replies", + on_delete=CASCADE, + ) versions = VersionRelation() class Meta: - verbose_name = _('comment') - verbose_name_plural = _('comments') + verbose_name = _("comment") + verbose_name_plural = _("comments") class MPTTMeta: - order_insertion_by = ['-time'] + order_insertion_by = ["-time"] @classmethod def most_recent(cls, user, n, batch=None): - queryset = cls.objects.filter(hidden=False).select_related('author__user') \ - .defer('author__about', 'body').order_by('-id') + queryset = ( + cls.objects.filter(hidden=False) + .select_related("author__user") + .defer("author__about", "body") + .order_by("-id") + ) - problem_access = CacheDict(lambda code: Problem.objects.get(code=code).is_accessible_by(user)) - contest_access = CacheDict(lambda key: Contest.objects.get(key=key).is_accessible_by(user)) + problem_access = CacheDict( + lambda code: Problem.objects.get(code=code).is_accessible_by(user) + ) + contest_access = CacheDict( + lambda key: Contest.objects.get(key=key).is_accessible_by(user) + ) blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user)) - + if n == -1: - n = len(queryset) + n = len(queryset) if user.is_superuser: return queryset[:n] if batch is None: batch = 2 * n output = [] for i in itertools.count(0): - slice = queryset[i * batch:i * batch + batch] + slice = queryset[i * batch : i * batch + batch] if not slice: break for comment in slice: - if comment.page.startswith('p:') or comment.page.startswith('s:'): + if comment.page.startswith("p:") or comment.page.startswith("s:"): try: if problem_access[comment.page[2:]]: output.append(comment) except Problem.DoesNotExist: pass - elif comment.page.startswith('c:'): + elif comment.page.startswith("c:"): try: if contest_access[comment.page[2:]]: output.append(comment) except Contest.DoesNotExist: pass - elif comment.page.startswith('b:'): + elif comment.page.startswith("b:"): try: if blog_access[comment.page[2:]]: output.append(comment) @@ -106,50 +127,55 @@ class Comment(MPTTModel): def link(self): try: link = None - if self.page.startswith('p:'): - link = reverse('problem_detail', args=(self.page[2:],)) - elif self.page.startswith('c:'): - link = reverse('contest_view', args=(self.page[2:],)) - elif self.page.startswith('b:'): - key = 'blog_slug:%s' % self.page[2:] + if self.page.startswith("p:"): + link = reverse("problem_detail", args=(self.page[2:],)) + elif self.page.startswith("c:"): + link = reverse("contest_view", args=(self.page[2:],)) + elif self.page.startswith("b:"): + key = "blog_slug:%s" % self.page[2:] slug = cache.get(key) if slug is None: try: slug = BlogPost.objects.get(id=self.page[2:]).slug except ObjectDoesNotExist: - slug = '' + slug = "" cache.set(key, slug, 3600) - link = reverse('blog_post', args=(self.page[2:], slug)) - elif self.page.startswith('s:'): - link = reverse('problem_editorial', args=(self.page[2:],)) + link = reverse("blog_post", args=(self.page[2:], slug)) + elif self.page.startswith("s:"): + link = reverse("problem_editorial", args=(self.page[2:],)) except Exception: - link = 'invalid' + link = "invalid" return link @classmethod def get_page_title(cls, page): try: - if page.startswith('p:'): - return Problem.objects.values_list('name', flat=True).get(code=page[2:]) - elif page.startswith('c:'): - return Contest.objects.values_list('name', flat=True).get(key=page[2:]) - elif page.startswith('b:'): - return BlogPost.objects.values_list('title', flat=True).get(id=page[2:]) - elif page.startswith('s:'): - return _('Editorial for %s') % Problem.objects.values_list('name', flat=True).get(code=page[2:]) - return '' + if page.startswith("p:"): + return Problem.objects.values_list("name", flat=True).get(code=page[2:]) + elif page.startswith("c:"): + return Contest.objects.values_list("name", flat=True).get(key=page[2:]) + elif page.startswith("b:"): + return BlogPost.objects.values_list("title", flat=True).get(id=page[2:]) + elif page.startswith("s:"): + return _("Editorial for %s") % Problem.objects.values_list( + "name", flat=True + ).get(code=page[2:]) + return "" except ObjectDoesNotExist: - return '' + return "" @cached_property def page_title(self): return self.get_page_title(self.page) def get_absolute_url(self): - return '%s#comment-%d' % (self.link, self.id) + return "%s#comment-%d" % (self.link, self.id) def __str__(self): - return '%(page)s by %(user)s' % {'page': self.page, 'user': self.author.user.username} + return "%(page)s by %(user)s" % { + "page": self.page, + "user": self.author.user.username, + } # Only use this when queried with # .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id))) @@ -165,34 +191,52 @@ class Comment(MPTTModel): class CommentVote(models.Model): - voter = models.ForeignKey(Profile, related_name='voted_comments', on_delete=CASCADE) - comment = models.ForeignKey(Comment, related_name='votes', on_delete=CASCADE) + voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE) + comment = models.ForeignKey(Comment, related_name="votes", on_delete=CASCADE) score = models.IntegerField() class Meta: - unique_together = ['voter', 'comment'] - verbose_name = _('comment vote') - verbose_name_plural = _('comment votes') + unique_together = ["voter", "comment"] + verbose_name = _("comment vote") + verbose_name_plural = _("comment votes") class CommentLock(models.Model): - page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True, - validators=[comment_validator]) + page = models.CharField( + max_length=30, + verbose_name=_("associated page"), + db_index=True, + validators=[comment_validator], + ) class Meta: - permissions = ( - ('override_comment_lock', _('Override comment lock')), - ) + permissions = (("override_comment_lock", _("Override comment lock")),) def __str__(self): return str(self.page) class Notification(models.Model): - owner = models.ForeignKey(Profile, verbose_name=_('owner'), related_name="notifications", on_delete=CASCADE) - time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) - comment = models.ForeignKey(Comment, null=True, verbose_name=_('comment'), on_delete=CASCADE) - read = models.BooleanField(verbose_name=_('read'), default=False) - category = models.CharField(verbose_name=_('category'), max_length=10) - html_link = models.TextField(default='', verbose_name=_('html link to comments, used for non-comments'), max_length=1000) - author = models.ForeignKey(Profile, null=True, verbose_name=_('who trigger, used for non-comment'), on_delete=CASCADE) \ No newline at end of file + owner = models.ForeignKey( + Profile, + verbose_name=_("owner"), + related_name="notifications", + on_delete=CASCADE, + ) + time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) + comment = models.ForeignKey( + Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE + ) + read = models.BooleanField(verbose_name=_("read"), default=False) + category = models.CharField(verbose_name=_("category"), max_length=10) + html_link = models.TextField( + default="", + verbose_name=_("html link to comments, used for non-comments"), + max_length=1000, + ) + author = models.ForeignKey( + Profile, + null=True, + verbose_name=_("who trigger, used for non-comment"), + on_delete=CASCADE, + ) diff --git a/judge/models/contest.py b/judge/models/contest.py index 74c32ce..ef92815 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -8,7 +8,13 @@ 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 moss import ( + MOSS_LANG_C, + MOSS_LANG_CC, + MOSS_LANG_JAVA, + MOSS_LANG_PYTHON, + MOSS_LANG_PASCAL, +) from judge import contest_format from judge.models.problem import Problem @@ -16,22 +22,39 @@ from judge.models.profile import Organization, Profile from judge.models.submission import Submission from judge.ratings import rate_contest -__all__ = ['Contest', 'ContestTag', 'ContestParticipation', 'ContestProblem', 'ContestSubmission', 'Rating'] +__all__ = [ + "Contest", + "ContestTag", + "ContestParticipation", + "ContestProblem", + "ContestSubmission", + "Rating", +] class ContestTag(models.Model): - color_validator = RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', _('Invalid colour.')) + color_validator = RegexValidator("^#(?:[A-Fa-f0-9]{3}){1,2}$", _("Invalid colour.")) - name = models.CharField(max_length=20, verbose_name=_('tag name'), unique=True, - validators=[RegexValidator(r'^[a-z-]+$', message=_('Lowercase letters and hyphens only.'))]) - color = models.CharField(max_length=7, verbose_name=_('tag colour'), validators=[color_validator]) - description = models.TextField(verbose_name=_('tag description'), blank=True) + name = models.CharField( + max_length=20, + verbose_name=_("tag name"), + unique=True, + validators=[ + RegexValidator( + r"^[a-z-]+$", message=_("Lowercase letters and hyphens only.") + ) + ], + ) + color = models.CharField( + max_length=7, verbose_name=_("tag colour"), validators=[color_validator] + ) + description = models.TextField(verbose_name=_("tag description"), blank=True) def __str__(self): return self.name def get_absolute_url(self): - return reverse('contest_tag', args=[self.name]) + return reverse("contest_tag", args=[self.name]) @property def text_color(self, cache={}): @@ -40,105 +63,232 @@ class ContestTag(models.Model): r, g, b = [ord(bytes.fromhex(i * 2)) for i in self.color[1:]] else: r, g, b = [i for i in bytes.fromhex(self.color[1:])] - cache[self.color] = '#000' if 299 * r + 587 * g + 144 * b > 140000 else '#fff' + cache[self.color] = ( + "#000" if 299 * r + 587 * g + 144 * b > 140000 else "#fff" + ) return cache[self.color] class Meta: - verbose_name = _('contest tag') - verbose_name_plural = _('contest tags') + verbose_name = _("contest tag") + verbose_name_plural = _("contest tags") class Contest(models.Model): - SCOREBOARD_VISIBLE = 'V' - SCOREBOARD_AFTER_CONTEST = 'C' - SCOREBOARD_AFTER_PARTICIPATION = 'P' + 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')), + (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) - 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) - end_time = models.DateTimeField(verbose_name=_('end time'), db_index=True) - time_limit = models.DurationField(verbose_name=_('time limit'), blank=True, null=True, - help_text=_('Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00')) - is_visible = models.BooleanField(verbose_name=_('publicly visible'), default=False, - help_text=_('Should be set even for organization-private contests, where it ' - 'determines whether the contest is visible to members of the ' - 'specified organizations.')) - is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'), - 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.')) - use_clarifications = models.BooleanField(verbose_name=_('no comments'), - help_text=_("Use clarification system instead of comments."), - default=True) - rating_floor = models.IntegerField(verbose_name=('rating floor'), help_text=_('Rating floor for contest'), - null=True, blank=True) - rating_ceiling = models.IntegerField(verbose_name=('rating ceiling'), help_text=_('Rating ceiling for contest'), - null=True, blank=True) - rate_all = models.BooleanField(verbose_name=_('rate all'), help_text=_('Rate all users who joined.'), default=False) - rate_exclude = models.ManyToManyField(Profile, verbose_name=_('exclude from ratings'), blank=True, - related_name='rate_exclude+') - is_private = models.BooleanField(verbose_name=_('private to specific users'), default=False) - private_contestants = models.ManyToManyField(Profile, blank=True, verbose_name=_('private contestants'), - help_text=_('If private, only these users may see the contest'), - related_name='private_contestants+') - hide_problem_tags = models.BooleanField(verbose_name=_('hide problem tags'), - help_text=_('Whether problem tags should be hidden by default.'), - default=True) - run_pretests_only = models.BooleanField(verbose_name=_('run pretests only'), - help_text=_('Whether judges should grade pretests only, versus all ' - 'testcases. Commonly set during a contest, then unset ' - 'prior to rejudging user submissions when the contest ends.'), - default=False) - is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False) - organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'), - help_text=_('If private, only these organizations may see the contest')) - og_image = models.CharField(verbose_name=_('OpenGraph image'), default='', max_length=150, blank=True) - logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150, - blank=True, - help_text=_('This image will replace the default site logo for users ' - 'inside the contest.')) - tags = models.ManyToManyField(ContestTag, verbose_name=_('contest tags'), blank=True, related_name='contests') - user_count = models.IntegerField(verbose_name=_('the amount of live participants'), default=0) - summary = models.TextField(blank=True, verbose_name=_('contest summary'), - help_text=_('Plain-text, shown in meta description tag, e.g. for social media.')) - access_code = models.CharField(verbose_name=_('access code'), blank=True, default='', max_length=255, - help_text=_('An optional code to prompt contestants before they are allowed ' - 'to join the contest. Leave it blank to disable.')) - banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True, - help_text=_('Bans the selected users from joining this contest.')) - format_name = models.CharField(verbose_name=_('contest format'), default='default', max_length=32, - choices=contest_format.choices(), help_text=_('The contest format module to use.')) - format_config = JSONField(verbose_name=_('contest format configuration'), null=True, blank=True, - 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.')) - + 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 + ) + 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) + end_time = models.DateTimeField(verbose_name=_("end time"), db_index=True) + time_limit = models.DurationField( + verbose_name=_("time limit"), + blank=True, + null=True, + help_text=_( + "Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00" + ), + ) + is_visible = models.BooleanField( + verbose_name=_("publicly visible"), + default=False, + help_text=_( + "Should be set even for organization-private contests, where it " + "determines whether the contest is visible to members of the " + "specified organizations." + ), + ) + is_rated = models.BooleanField( + verbose_name=_("contest rated"), + help_text=_("Whether this contest can be rated."), + 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."), + ) + use_clarifications = models.BooleanField( + verbose_name=_("no comments"), + help_text=_("Use clarification system instead of comments."), + default=True, + ) + rating_floor = models.IntegerField( + verbose_name=("rating floor"), + help_text=_("Rating floor for contest"), + null=True, + blank=True, + ) + rating_ceiling = models.IntegerField( + verbose_name=("rating ceiling"), + help_text=_("Rating ceiling for contest"), + null=True, + blank=True, + ) + rate_all = models.BooleanField( + verbose_name=_("rate all"), + help_text=_("Rate all users who joined."), + default=False, + ) + rate_exclude = models.ManyToManyField( + Profile, + verbose_name=_("exclude from ratings"), + blank=True, + related_name="rate_exclude+", + ) + is_private = models.BooleanField( + verbose_name=_("private to specific users"), default=False + ) + private_contestants = models.ManyToManyField( + Profile, + blank=True, + verbose_name=_("private contestants"), + help_text=_("If private, only these users may see the contest"), + related_name="private_contestants+", + ) + hide_problem_tags = models.BooleanField( + verbose_name=_("hide problem tags"), + help_text=_("Whether problem tags should be hidden by default."), + default=True, + ) + run_pretests_only = models.BooleanField( + verbose_name=_("run pretests only"), + help_text=_( + "Whether judges should grade pretests only, versus all " + "testcases. Commonly set during a contest, then unset " + "prior to rejudging user submissions when the contest ends." + ), + default=False, + ) + is_organization_private = models.BooleanField( + verbose_name=_("private to organizations"), default=False + ) + organizations = models.ManyToManyField( + Organization, + blank=True, + verbose_name=_("organizations"), + help_text=_("If private, only these organizations may see the contest"), + ) + og_image = models.CharField( + verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True + ) + logo_override_image = models.CharField( + verbose_name=_("Logo override image"), + default="", + max_length=150, + blank=True, + help_text=_( + "This image will replace the default site logo for users " + "inside the contest." + ), + ) + tags = models.ManyToManyField( + ContestTag, verbose_name=_("contest tags"), blank=True, related_name="contests" + ) + user_count = models.IntegerField( + verbose_name=_("the amount of live participants"), default=0 + ) + summary = models.TextField( + blank=True, + verbose_name=_("contest summary"), + help_text=_( + "Plain-text, shown in meta description tag, e.g. for social media." + ), + ) + access_code = models.CharField( + verbose_name=_("access code"), + blank=True, + default="", + max_length=255, + help_text=_( + "An optional code to prompt contestants before they are allowed " + "to join the contest. Leave it blank to disable." + ), + ) + banned_users = models.ManyToManyField( + Profile, + verbose_name=_("personae non gratae"), + blank=True, + help_text=_("Bans the selected users from joining this contest."), + ) + format_name = models.CharField( + verbose_name=_("contest format"), + default="default", + max_length=32, + choices=contest_format.choices(), + help_text=_("The contest format module to use."), + ) + format_config = JSONField( + verbose_name=_("contest format configuration"), + null=True, + blank=True, + 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."), + ) + @cached_property def format_class(self): return contest_format.formats[self.format_name] @@ -151,13 +301,20 @@ class Contest(models.Model): 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()) + + 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?') + raise ValidationError( + "What is this? A contest that ended before it starts?" + ) self.format_class.validate(self.format_config) try: @@ -165,15 +322,21 @@ class Contest(models.Model): # 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) + 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.') + 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 ( + profile + and profile.current_contest is not None + and profile.current_contest.contest == self + ) return False def can_see_own_scoreboard(self, user): @@ -190,19 +353,26 @@ class Contest(models.Model): 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'): + 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): + 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() + participation = self.users.filter( + virtual=ContestParticipation.LIVE, user=user.profile + ).first() if participation and participation.ended: return True return False @@ -211,8 +381,11 @@ class Contest(models.Model): 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): + if ( + self.scoreboard_visibility + in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) + and not self.ended + ): return False return True @@ -249,22 +422,29 @@ class Contest(models.Model): @cached_property def author_ids(self): - return Contest.authors.through.objects.filter(contest=self).values_list('profile_id', flat=True) + 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)) + 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) + return Contest.testers.through.objects.filter(contest=self).values_list( + "profile_id", flat=True + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('contest_view', args=(self.key,)) + return reverse("contest_view", args=(self.key,)) def update_user_count(self): self.user_count = self.users.filter(virtual=0).count() @@ -289,7 +469,9 @@ class Contest(models.Model): 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'): + if user.has_perm("judge.see_private_contest") or user.has_perm( + "judge.edit_all_contest" + ): return # User is organizer or curator for contest @@ -310,14 +492,16 @@ class Contest(models.Model): 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_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() + raise self.PrivateContest() if self.is_private and not self.is_organization_private: if in_users: @@ -339,11 +523,14 @@ class Contest(models.Model): def is_editable_by(self, user): # If the user can edit all contests - if user.has_perm('judge.edit_all_contest'): + if user.has_perm("judge.edit_all_contest"): return True # 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: + if ( + user.has_perm("judge.edit_own_contest") + and user.profile.id in self.editor_ids + ): return True return False @@ -351,19 +538,39 @@ class Contest(models.Model): @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() + 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')): + 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(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) @@ -373,51 +580,75 @@ class Contest(models.Model): return queryset.distinct() def rate(self): - Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete() + Rating.objects.filter( + contest__end_time__range=(self.end_time, self._now) + ).delete() for contest in Contest.objects.filter( - is_rated=True, end_time__range=(self.end_time, self._now), - ).order_by('end_time'): + is_rated=True, + end_time__range=(self.end_time, self._now), + ).order_by("end_time"): rate_contest(contest) class Meta: 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')), + ("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') + verbose_name = _("contest") + verbose_name_plural = _("contests") class ContestParticipation(models.Model): LIVE = 0 SPECTATE = -1 - contest = models.ForeignKey(Contest, verbose_name=_('associated contest'), related_name='users', on_delete=CASCADE) - user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='contest_history', on_delete=CASCADE) - real_start = models.DateTimeField(verbose_name=_('start time'), default=timezone.now, db_column='start') - score = models.FloatField(verbose_name=_('score'), default=0, db_index=True) - 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) + contest = models.ForeignKey( + Contest, + verbose_name=_("associated contest"), + related_name="users", + on_delete=CASCADE, + ) + user = models.ForeignKey( + Profile, + verbose_name=_("user"), + related_name="contest_history", + on_delete=CASCADE, + ) + real_start = models.DateTimeField( + verbose_name=_("start time"), default=timezone.now, db_column="start" + ) + score = models.FloatField(verbose_name=_("score"), default=0, db_index=True) + 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 + ) def recompute_results(self): with transaction.atomic(): self.contest.format.update_participation(self) if self.is_disqualified: self.score = -9999 - self.save(update_fields=['score']) + self.save(update_fields=["score"]) + recompute_results.alters_data = True def set_disqualified(self, disqualified): @@ -431,6 +662,7 @@ class ContestParticipation(models.Model): self.contest.banned_users.add(self.user) else: self.contest.banned_users.remove(self.user) + set_disqualified.alters_data = True @property @@ -444,7 +676,11 @@ class ContestParticipation(models.Model): @cached_property def start(self): contest = self.contest - return contest.start_time if contest.time_limit is None and (self.live or self.spectate) else self.real_start + return ( + contest.start_time + if contest.time_limit is None and (self.live or self.spectate) + else self.real_start + ) @cached_property def end_time(self): @@ -456,8 +692,11 @@ class ContestParticipation(models.Model): return self.real_start + contest.time_limit else: return self.real_start + (contest.end_time - contest.start_time) - return contest.end_time if contest.time_limit is None else \ - min(self.real_start + contest.time_limit, contest.end_time) + return ( + contest.end_time + if contest.time_limit is None + else min(self.real_start + contest.time_limit, contest.end_time) + ) @cached_property def _now(self): @@ -476,88 +715,140 @@ class ContestParticipation(models.Model): def __str__(self): if self.spectate: - return gettext('%s spectating in %s') % (self.user.username, self.contest.name) + return gettext("%s spectating in %s") % ( + self.user.username, + self.contest.name, + ) if self.virtual: - return gettext('%s in %s, v%d') % (self.user.username, self.contest.name, self.virtual) - return gettext('%s in %s') % (self.user.username, self.contest.name) + return gettext("%s in %s, v%d") % ( + self.user.username, + self.contest.name, + self.virtual, + ) + return gettext("%s in %s") % (self.user.username, self.contest.name) class Meta: - verbose_name = _('contest participation') - verbose_name_plural = _('contest participations') + verbose_name = _("contest participation") + verbose_name_plural = _("contest participations") - unique_together = ('contest', 'user', 'virtual') + unique_together = ("contest", "user", "virtual") class ContestProblem(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='contests', on_delete=CASCADE) - contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='contest_problems', on_delete=CASCADE) - points = models.IntegerField(verbose_name=_('points')) - partial = models.BooleanField(default=True, verbose_name=_('partial')) - is_pretested = models.BooleanField(default=False, verbose_name=_('is pretested')) - order = models.PositiveIntegerField(db_index=True, verbose_name=_('order')) - output_prefix_override = models.IntegerField(help_text=_('0 to not show testcases, 1 to show'), - verbose_name=_('visible testcases'), null=True, blank=True, default=0) - max_submissions = models.IntegerField(help_text=_('Maximum number of submissions for this problem, ' - 'or 0 for no limit.'), default=0, - validators=[MinValueValidator(0, _('Why include a problem you ' - 'can\'t submit to?'))]) + problem = models.ForeignKey( + Problem, verbose_name=_("problem"), related_name="contests", on_delete=CASCADE + ) + contest = models.ForeignKey( + Contest, + verbose_name=_("contest"), + related_name="contest_problems", + on_delete=CASCADE, + ) + points = models.IntegerField(verbose_name=_("points")) + partial = models.BooleanField(default=True, verbose_name=_("partial")) + is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested")) + order = models.PositiveIntegerField(db_index=True, verbose_name=_("order")) + output_prefix_override = models.IntegerField( + help_text=_("0 to not show testcases, 1 to show"), + verbose_name=_("visible testcases"), + null=True, + blank=True, + default=0, + ) + max_submissions = models.IntegerField( + help_text=_( + "Maximum number of submissions for this problem, " "or 0 for no limit." + ), + default=0, + validators=[ + MinValueValidator(0, _("Why include a problem you " "can't submit to?")) + ], + ) class Meta: - unique_together = ('problem', 'contest') - verbose_name = _('contest problem') - verbose_name_plural = _('contest problems') + unique_together = ("problem", "contest") + verbose_name = _("contest problem") + verbose_name_plural = _("contest problems") class ContestSubmission(models.Model): - submission = models.OneToOneField(Submission, verbose_name=_('submission'), - related_name='contest', on_delete=CASCADE) - problem = models.ForeignKey(ContestProblem, verbose_name=_('problem'), on_delete=CASCADE, - related_name='submissions', related_query_name='submission') - participation = models.ForeignKey(ContestParticipation, verbose_name=_('participation'), on_delete=CASCADE, - related_name='submissions', related_query_name='submission') - points = models.FloatField(default=0.0, verbose_name=_('points')) - is_pretest = models.BooleanField(verbose_name=_('is pretested'), - help_text=_('Whether this submission was ran only on pretests.'), - default=False) + submission = models.OneToOneField( + Submission, + verbose_name=_("submission"), + related_name="contest", + on_delete=CASCADE, + ) + problem = models.ForeignKey( + ContestProblem, + verbose_name=_("problem"), + on_delete=CASCADE, + related_name="submissions", + related_query_name="submission", + ) + participation = models.ForeignKey( + ContestParticipation, + verbose_name=_("participation"), + on_delete=CASCADE, + related_name="submissions", + related_query_name="submission", + ) + points = models.FloatField(default=0.0, verbose_name=_("points")) + is_pretest = models.BooleanField( + verbose_name=_("is pretested"), + help_text=_("Whether this submission was ran only on pretests."), + default=False, + ) class Meta: - verbose_name = _('contest submission') - verbose_name_plural = _('contest submissions') + verbose_name = _("contest submission") + verbose_name_plural = _("contest submissions") class Rating(models.Model): - user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='ratings', on_delete=CASCADE) - contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='ratings', on_delete=CASCADE) - participation = models.OneToOneField(ContestParticipation, verbose_name=_('participation'), - related_name='rating', on_delete=CASCADE) - rank = models.IntegerField(verbose_name=_('rank')) - rating = models.IntegerField(verbose_name=_('rating')) - 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')) + user = models.ForeignKey( + Profile, verbose_name=_("user"), related_name="ratings", on_delete=CASCADE + ) + contest = models.ForeignKey( + Contest, verbose_name=_("contest"), related_name="ratings", on_delete=CASCADE + ) + participation = models.OneToOneField( + ContestParticipation, + verbose_name=_("participation"), + related_name="rating", + on_delete=CASCADE, + ) + rank = models.IntegerField(verbose_name=_("rank")) + rating = models.IntegerField(verbose_name=_("rating")) + 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: - unique_together = ('user', 'contest') - verbose_name = _('contest rating') - verbose_name_plural = _('contest ratings') + unique_together = ("user", "contest") + verbose_name = _("contest rating") + verbose_name_plural = _("contest ratings") class ContestMoss(models.Model): LANG_MAPPING = [ - ('C', MOSS_LANG_C), - ('C++', MOSS_LANG_CC), - ('Java', MOSS_LANG_JAVA), - ('Python', MOSS_LANG_PYTHON), - ('Pascal', MOSS_LANG_PASCAL), + ("C", MOSS_LANG_C), + ("C++", MOSS_LANG_CC), + ("Java", MOSS_LANG_JAVA), + ("Python", MOSS_LANG_PYTHON), + ("Pascal", MOSS_LANG_PASCAL), ] - contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='moss', on_delete=CASCADE) - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='moss', on_delete=CASCADE) + contest = models.ForeignKey( + Contest, verbose_name=_("contest"), related_name="moss", on_delete=CASCADE + ) + problem = models.ForeignKey( + Problem, verbose_name=_("problem"), related_name="moss", on_delete=CASCADE + ) language = models.CharField(max_length=10) submission_count = models.PositiveIntegerField(default=0) url = models.URLField(null=True, blank=True) class Meta: - unique_together = ('contest', 'problem', 'language') - verbose_name = _('contest moss result') - verbose_name_plural = _('contest moss results') + unique_together = ("contest", "problem", "language") + verbose_name = _("contest moss result") + verbose_name_plural = _("contest moss results") diff --git a/judge/models/interface.py b/judge/models/interface.py index 6d94d52..24b05de 100644 --- a/judge/models/interface.py +++ b/judge/models/interface.py @@ -10,7 +10,7 @@ from mptt.models import MPTTModel from judge.models.profile import Organization, Profile -__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost'] +__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"] class MiscConfig(models.Model): @@ -21,32 +21,40 @@ class MiscConfig(models.Model): return self.key class Meta: - verbose_name = _('configuration item') - verbose_name_plural = _('miscellaneous configuration') + verbose_name = _("configuration item") + verbose_name_plural = _("miscellaneous configuration") def validate_regex(regex): try: re.compile(regex, re.VERBOSE) except re.error as e: - raise ValidationError('Invalid regex: %s' % e.message) + raise ValidationError("Invalid regex: %s" % e.message) class NavigationBar(MPTTModel): class Meta: - verbose_name = _('navigation item') - verbose_name_plural = _('navigation bar') + verbose_name = _("navigation item") + verbose_name_plural = _("navigation bar") class MPTTMeta: - order_insertion_by = ['order'] + order_insertion_by = ["order"] - order = models.PositiveIntegerField(db_index=True, verbose_name=_('order')) - key = models.CharField(max_length=10, unique=True, verbose_name=_('identifier')) - label = models.CharField(max_length=20, verbose_name=_('label')) - path = models.CharField(max_length=255, verbose_name=_('link path')) - regex = models.TextField(verbose_name=_('highlight regex'), validators=[validate_regex]) - parent = TreeForeignKey('self', verbose_name=_('parent item'), null=True, blank=True, - related_name='children', on_delete=models.CASCADE) + order = models.PositiveIntegerField(db_index=True, verbose_name=_("order")) + key = models.CharField(max_length=10, unique=True, verbose_name=_("identifier")) + label = models.CharField(max_length=20, verbose_name=_("label")) + path = models.CharField(max_length=255, verbose_name=_("link path")) + regex = models.TextField( + verbose_name=_("highlight regex"), validators=[validate_regex] + ) + parent = TreeForeignKey( + "self", + verbose_name=_("parent item"), + null=True, + blank=True, + related_name="children", + on_delete=models.CASCADE, + ) def __str__(self): return self.label @@ -63,46 +71,61 @@ class NavigationBar(MPTTModel): class BlogPost(models.Model): - title = models.CharField(verbose_name=_('post title'), max_length=100) - authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True) - slug = models.SlugField(verbose_name=_('slug')) - visible = models.BooleanField(verbose_name=_('public visibility'), default=False) - sticky = models.BooleanField(verbose_name=_('sticky'), default=False) - publish_on = models.DateTimeField(verbose_name=_('publish after')) - content = models.TextField(verbose_name=_('post content')) - summary = models.TextField(verbose_name=_('post summary'), blank=True) - og_image = models.CharField(verbose_name=_('openGraph image'), default='', max_length=150, blank=True) - organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'), - help_text=_('If private, only these organizations may see the blog post.')) - is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False) + title = models.CharField(verbose_name=_("post title"), max_length=100) + authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True) + slug = models.SlugField(verbose_name=_("slug")) + visible = models.BooleanField(verbose_name=_("public visibility"), default=False) + sticky = models.BooleanField(verbose_name=_("sticky"), default=False) + publish_on = models.DateTimeField(verbose_name=_("publish after")) + content = models.TextField(verbose_name=_("post content")) + summary = models.TextField(verbose_name=_("post summary"), blank=True) + og_image = models.CharField( + verbose_name=_("openGraph image"), default="", max_length=150, blank=True + ) + organizations = models.ManyToManyField( + Organization, + blank=True, + verbose_name=_("organizations"), + help_text=_("If private, only these organizations may see the blog post."), + ) + is_organization_private = models.BooleanField( + verbose_name=_("private to organizations"), default=False + ) def __str__(self): return self.title def get_absolute_url(self): - return reverse('blog_post', args=(self.id, self.slug)) + return reverse("blog_post", args=(self.id, self.slug)) def can_see(self, user): if self.visible and self.publish_on <= timezone.now(): if not self.is_organization_private: return True - if user.is_authenticated and \ - self.organizations.filter(id__in=user.profile.organizations.all()).exists(): + if ( + user.is_authenticated + and self.organizations.filter( + id__in=user.profile.organizations.all() + ).exists() + ): return True - if user.has_perm('judge.edit_all_post'): + if user.has_perm("judge.edit_all_post"): return True - return user.is_authenticated and self.authors.filter(id=user.profile.id).exists() + return ( + user.is_authenticated and self.authors.filter(id=user.profile.id).exists() + ) def is_editable_by(self, user): - if not user.is_authenticated: - return False - if user.has_perm('judge.edit_all_post'): - return True - return user.has_perm('judge.change_blogpost') and self.authors.filter(id=user.profile.id).exists() + if not user.is_authenticated: + return False + if user.has_perm("judge.edit_all_post"): + return True + return ( + user.has_perm("judge.change_blogpost") + and self.authors.filter(id=user.profile.id).exists() + ) class Meta: - permissions = ( - ('edit_all_post', _('Edit all posts')), - ) - verbose_name = _('blog post') - verbose_name_plural = _('blog posts') + permissions = (("edit_all_post", _("Edit all posts")),) + verbose_name = _("blog post") + verbose_name_plural = _("blog posts") diff --git a/judge/models/message.py b/judge/models/message.py index 541d63f..c2e8291 100644 --- a/judge/models/message.py +++ b/judge/models/message.py @@ -4,17 +4,31 @@ from django.utils.translation import gettext_lazy as _ from judge.models.profile import Profile -__all__ = ['PrivateMessage', 'PrivateMessageThread'] +__all__ = ["PrivateMessage", "PrivateMessageThread"] class PrivateMessage(models.Model): - title = models.CharField(verbose_name=_('message title'), max_length=50) - content = models.TextField(verbose_name=_('message body')) - sender = models.ForeignKey(Profile, verbose_name=_('sender'), related_name='sent_messages', on_delete=CASCADE) - target = models.ForeignKey(Profile, verbose_name=_('target'), related_name='received_messages', on_delete=CASCADE) - timestamp = models.DateTimeField(verbose_name=_('message timestamp'), auto_now_add=True) - read = models.BooleanField(verbose_name=_('read'), default=False) + title = models.CharField(verbose_name=_("message title"), max_length=50) + content = models.TextField(verbose_name=_("message body")) + sender = models.ForeignKey( + Profile, + verbose_name=_("sender"), + related_name="sent_messages", + on_delete=CASCADE, + ) + target = models.ForeignKey( + Profile, + verbose_name=_("target"), + related_name="received_messages", + on_delete=CASCADE, + ) + timestamp = models.DateTimeField( + verbose_name=_("message timestamp"), auto_now_add=True + ) + read = models.BooleanField(verbose_name=_("read"), default=False) class PrivateMessageThread(models.Model): - messages = models.ManyToManyField(PrivateMessage, verbose_name=_('messages in the thread')) + messages = models.ManyToManyField( + PrivateMessage, verbose_name=_("messages in the thread") + ) diff --git a/judge/models/problem.py b/judge/models/problem.py index ce66da5..bb99ae3 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -19,143 +19,286 @@ from judge.models.runtime import Language from judge.user_translations import gettext as user_gettext from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join -__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification', - 'License', 'Solution', 'TranslatedProblemQuerySet', 'TranslatedProblemForeignKeyQuerySet'] +__all__ = [ + "ProblemGroup", + "ProblemType", + "Problem", + "ProblemTranslation", + "ProblemClarification", + "License", + "Solution", + "TranslatedProblemQuerySet", + "TranslatedProblemForeignKeyQuerySet", +] class ProblemType(models.Model): - name = models.CharField(max_length=20, verbose_name=_('problem category ID'), unique=True) - full_name = models.CharField(max_length=100, verbose_name=_('problem category name')) + name = models.CharField( + max_length=20, verbose_name=_("problem category ID"), unique=True + ) + full_name = models.CharField( + max_length=100, verbose_name=_("problem category name") + ) def __str__(self): return self.full_name class Meta: - ordering = ['full_name'] - verbose_name = _('problem type') - verbose_name_plural = _('problem types') + ordering = ["full_name"] + verbose_name = _("problem type") + verbose_name_plural = _("problem types") class ProblemGroup(models.Model): - name = models.CharField(max_length=20, verbose_name=_('problem group ID'), unique=True) - full_name = models.CharField(max_length=100, verbose_name=_('problem group name')) + name = models.CharField( + max_length=20, verbose_name=_("problem group ID"), unique=True + ) + full_name = models.CharField(max_length=100, verbose_name=_("problem group name")) def __str__(self): return self.full_name class Meta: - ordering = ['full_name'] - verbose_name = _('problem group') - verbose_name_plural = _('problem groups') + ordering = ["full_name"] + verbose_name = _("problem group") + verbose_name_plural = _("problem groups") class License(models.Model): - key = models.CharField(max_length=20, unique=True, verbose_name=_('key'), - validators=[RegexValidator(r'^[-\w.]+$', r'License key must be ^[-\w.]+$')]) - link = models.CharField(max_length=256, verbose_name=_('link')) - name = models.CharField(max_length=256, verbose_name=_('full name')) - display = models.CharField(max_length=256, blank=True, verbose_name=_('short name'), - help_text=_('Displayed on pages under this license')) - icon = models.CharField(max_length=256, blank=True, verbose_name=_('icon'), help_text=_('URL to the icon')) - text = models.TextField(verbose_name=_('license text')) + key = models.CharField( + max_length=20, + unique=True, + verbose_name=_("key"), + validators=[RegexValidator(r"^[-\w.]+$", r"License key must be ^[-\w.]+$")], + ) + link = models.CharField(max_length=256, verbose_name=_("link")) + name = models.CharField(max_length=256, verbose_name=_("full name")) + display = models.CharField( + max_length=256, + blank=True, + verbose_name=_("short name"), + help_text=_("Displayed on pages under this license"), + ) + icon = models.CharField( + max_length=256, + blank=True, + verbose_name=_("icon"), + help_text=_("URL to the icon"), + ) + text = models.TextField(verbose_name=_("license text")) def __str__(self): return self.name def get_absolute_url(self): - return reverse('license', args=(self.key,)) + return reverse("license", args=(self.key,)) class Meta: - verbose_name = _('license') - verbose_name_plural = _('licenses') + verbose_name = _("license") + verbose_name_plural = _("licenses") class TranslatedProblemQuerySet(SearchQuerySet): def __init__(self, **kwargs): - super(TranslatedProblemQuerySet, self).__init__(('code', 'name', 'description'), **kwargs) + super(TranslatedProblemQuerySet, self).__init__( + ("code", "name", "description"), **kwargs + ) def add_i18n_name(self, language): queryset = self._clone() - alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language) - return queryset.annotate(i18n_name=Coalesce(RawSQL('%s.name' % alias, ()), F('name'), - output_field=models.CharField())) + alias = unique_together_left_join( + queryset, ProblemTranslation, "problem", "language", language + ) + return queryset.annotate( + i18n_name=Coalesce( + RawSQL("%s.name" % alias, ()), + F("name"), + output_field=models.CharField(), + ) + ) class TranslatedProblemForeignKeyQuerySet(QuerySet): def add_problem_i18n_name(self, key, language, name_field=None): - queryset = self._clone() if name_field is None else self.annotate(_name=F(name_field)) - alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language, - parent_model=Problem) + queryset = ( + self._clone() if name_field is None else self.annotate(_name=F(name_field)) + ) + alias = unique_together_left_join( + queryset, + ProblemTranslation, + "problem", + "language", + language, + parent_model=Problem, + ) # You must specify name_field if Problem is not yet joined into the QuerySet. - kwargs = {key: Coalesce(RawSQL('%s.name' % alias, ()), - F(name_field) if name_field else RawSQLColumn(Problem, 'name'), - output_field=models.CharField())} + kwargs = { + key: Coalesce( + RawSQL("%s.name" % alias, ()), + F(name_field) if name_field else RawSQLColumn(Problem, "name"), + output_field=models.CharField(), + ) + } return queryset.annotate(**kwargs) class Problem(models.Model): - code = models.CharField(max_length=20, verbose_name=_('problem code'), unique=True, - validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))], - help_text=_('A short, unique code for the problem, ' - 'used in the url after /problem/')) - name = models.CharField(max_length=100, verbose_name=_('problem name'), db_index=True, - help_text=_('The full name of the problem, ' - 'as shown in the problem list.')) - description = models.TextField(verbose_name=_('problem body')) - authors = models.ManyToManyField(Profile, verbose_name=_('creators'), blank=True, related_name='authored_problems', - help_text=_('These users will be able to edit the problem, ' - 'and be listed as authors.')) - curators = models.ManyToManyField(Profile, verbose_name=_('curators'), blank=True, related_name='curated_problems', - help_text=_('These users will be able to edit the problem, ' - 'but not be listed as authors.')) - testers = models.ManyToManyField(Profile, verbose_name=_('testers'), blank=True, related_name='tested_problems', - help_text=_( - 'These users will be able to view the private problem, but not edit it.')) - types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'), - help_text=_('The type of problem, ' - "as shown on the problem's page.")) - group = models.ForeignKey(ProblemGroup, verbose_name=_('problem group'), on_delete=CASCADE, - help_text=_('The group of problem, shown under Category in the problem list.')) - time_limit = models.FloatField(verbose_name=_('time limit'), - help_text=_('The time limit for this problem, in seconds. ' - 'Fractional seconds (e.g. 1.5) are supported.'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)]) - memory_limit = models.PositiveIntegerField(verbose_name=_('memory limit'), - help_text=_('The memory limit for this problem, in kilobytes ' - '(e.g. 64mb = 65536 kilobytes).'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)]) + code = models.CharField( + max_length=20, + verbose_name=_("problem code"), + unique=True, + validators=[ + RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$")) + ], + help_text=_( + "A short, unique code for the problem, " "used in the url after /problem/" + ), + ) + name = models.CharField( + max_length=100, + verbose_name=_("problem name"), + db_index=True, + help_text=_("The full name of the problem, " "as shown in the problem list."), + ) + description = models.TextField(verbose_name=_("problem body")) + authors = models.ManyToManyField( + Profile, + verbose_name=_("creators"), + blank=True, + related_name="authored_problems", + help_text=_( + "These users will be able to edit the problem, " "and be listed as authors." + ), + ) + curators = models.ManyToManyField( + Profile, + verbose_name=_("curators"), + blank=True, + related_name="curated_problems", + help_text=_( + "These users will be able to edit the problem, " + "but not be listed as authors." + ), + ) + testers = models.ManyToManyField( + Profile, + verbose_name=_("testers"), + blank=True, + related_name="tested_problems", + help_text=_( + "These users will be able to view the private problem, but not edit it." + ), + ) + types = models.ManyToManyField( + ProblemType, + verbose_name=_("problem types"), + help_text=_("The type of problem, " "as shown on the problem's page."), + ) + group = models.ForeignKey( + ProblemGroup, + verbose_name=_("problem group"), + on_delete=CASCADE, + help_text=_("The group of problem, shown under Category in the problem list."), + ) + time_limit = models.FloatField( + verbose_name=_("time limit"), + help_text=_( + "The time limit for this problem, in seconds. " + "Fractional seconds (e.g. 1.5) are supported." + ), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT), + ], + ) + memory_limit = models.PositiveIntegerField( + verbose_name=_("memory limit"), + help_text=_( + "The memory limit for this problem, in kilobytes " + "(e.g. 64mb = 65536 kilobytes)." + ), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT), + ], + ) short_circuit = models.BooleanField(default=False) - points = models.FloatField(verbose_name=_('points'), - help_text=_('Points awarded for problem completion. ' - "Points are displayed with a 'p' suffix if partial."), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)]) - partial = models.BooleanField(verbose_name=_('allows partial points'), default=False) - allowed_languages = models.ManyToManyField(Language, verbose_name=_('allowed languages'), - help_text=_('List of allowed submission languages.')) - is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False) - is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False, - help_text=_('Whether judges should be allowed to manage data or not.')) - date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True, - help_text=_("Doesn't have magic ability to auto-publish due to backward compatibility")) - banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True, - help_text=_('Bans the selected users from submitting to this problem.')) - license = models.ForeignKey(License, null=True, blank=True, on_delete=SET_NULL, - help_text=_('The license under which this problem is published.')) - og_image = models.CharField(verbose_name=_('OpenGraph image'), max_length=150, blank=True) - summary = models.TextField(blank=True, verbose_name=_('problem summary'), - help_text=_('Plain-text, shown in meta description tag, e.g. for social media.')) - user_count = models.IntegerField(verbose_name=_('number of users'), default=0, - help_text=_('The number of users who solved the problem.')) - ac_rate = models.FloatField(verbose_name=_('solve rate'), default=0) + points = models.FloatField( + verbose_name=_("points"), + help_text=_( + "Points awarded for problem completion. " + "Points are displayed with a 'p' suffix if partial." + ), + validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)], + ) + partial = models.BooleanField( + verbose_name=_("allows partial points"), default=False + ) + allowed_languages = models.ManyToManyField( + Language, + verbose_name=_("allowed languages"), + help_text=_("List of allowed submission languages."), + ) + is_public = models.BooleanField( + verbose_name=_("publicly visible"), db_index=True, default=False + ) + is_manually_managed = models.BooleanField( + verbose_name=_("manually managed"), + db_index=True, + default=False, + help_text=_("Whether judges should be allowed to manage data or not."), + ) + date = models.DateTimeField( + verbose_name=_("date of publishing"), + null=True, + blank=True, + db_index=True, + help_text=_( + "Doesn't have magic ability to auto-publish due to backward compatibility" + ), + ) + banned_users = models.ManyToManyField( + Profile, + verbose_name=_("personae non gratae"), + blank=True, + help_text=_("Bans the selected users from submitting to this problem."), + ) + license = models.ForeignKey( + License, + null=True, + blank=True, + on_delete=SET_NULL, + help_text=_("The license under which this problem is published."), + ) + og_image = models.CharField( + verbose_name=_("OpenGraph image"), max_length=150, blank=True + ) + summary = models.TextField( + blank=True, + verbose_name=_("problem summary"), + help_text=_( + "Plain-text, shown in meta description tag, e.g. for social media." + ), + ) + user_count = models.IntegerField( + verbose_name=_("number of users"), + default=0, + help_text=_("The number of users who solved the problem."), + ) + ac_rate = models.FloatField(verbose_name=_("solve rate"), default=0) objects = TranslatedProblemQuerySet.as_manager() - tickets = GenericRelation('Ticket') + tickets = GenericRelation("Ticket") - organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'), - help_text=_('If private, only these organizations may see the problem.')) - is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False) + organizations = models.ManyToManyField( + Organization, + blank=True, + verbose_name=_("organizations"), + help_text=_("If private, only these organizations may see the problem."), + ) + is_organization_private = models.BooleanField( + verbose_name=_("private to organizations"), default=False + ) def __init__(self, *args, **kwargs): super(Problem, self).__init__(*args, **kwargs) @@ -165,20 +308,30 @@ class Problem(models.Model): @cached_property def types_list(self): - return list(map(user_gettext, map(attrgetter('full_name'), self.types.all()))) + return list(map(user_gettext, map(attrgetter("full_name"), self.types.all()))) def languages_list(self): - return self.allowed_languages.values_list('common_name', flat=True).distinct().order_by('common_name') + return ( + self.allowed_languages.values_list("common_name", flat=True) + .distinct() + .order_by("common_name") + ) def is_editor(self, profile): - return (self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)).exists() + return ( + self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id) + ).exists() def is_editable_by(self, user): if not user.is_authenticated: return False - if user.has_perm('judge.edit_all_problem') or user.has_perm('judge.edit_public_problem') and self.is_public: + if ( + user.has_perm("judge.edit_all_problem") + or user.has_perm("judge.edit_public_problem") + and self.is_public + ): return True - return user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile) + return user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile) def is_accessible_by(self, user): # Problem is public. @@ -188,23 +341,24 @@ class Problem(models.Model): return True # If the user can see all organization private problems. - if user.has_perm('judge.see_organization_problem'): + if user.has_perm("judge.see_organization_problem"): return True # If the user is in the organization. - if user.is_authenticated and \ - self.organizations.filter(id__in=user.profile.organizations.all()): + if user.is_authenticated and self.organizations.filter( + id__in=user.profile.organizations.all() + ): return True # If the user can view all problems. - if user.has_perm('judge.see_private_problem'): + if user.has_perm("judge.see_private_problem"): return True if not user.is_authenticated: return False # If the user authored the problem or is a curator. - if user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile): + if user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile): return True # If user is a tester. @@ -216,11 +370,18 @@ class Problem(models.Model): if current is None: return False from judge.models import ContestProblem - return ContestProblem.objects.filter(problem_id=self.id, contest__users__id=current).exists() + + return ContestProblem.objects.filter( + problem_id=self.id, contest__users__id=current + ).exists() def is_subs_manageable_by(self, user): - return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user) - + return ( + user.is_staff + and user.has_perm("judge.rejudge_submission") + and self.is_editable_by(user) + ) + @classmethod def get_visible_problems(cls, user): # Do unauthenticated check here so we can skip authentication checks later on. @@ -235,15 +396,18 @@ class Problem(models.Model): # - 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') + queryset = cls.objects.defer("description") - if not (user.has_perm('judge.see_private_problem') or user.has_perm('judge.edit_all_problem')): + 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'): + 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()) + 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. @@ -256,40 +420,46 @@ class Problem(models.Model): @classmethod def get_public_problems(cls): - return cls.objects.filter(is_public=True, is_organization_private=False).defer('description') + return cls.objects.filter(is_public=True, is_organization_private=False).defer( + "description" + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('problem_detail', args=(self.code,)) + return reverse("problem_detail", args=(self.code,)) @cached_property def author_ids(self): - return self.authors.values_list('id', flat=True) + return self.authors.values_list("id", flat=True) @cached_property def editor_ids(self): - return self.author_ids | self.curators.values_list('id', flat=True) + return self.author_ids | self.curators.values_list("id", flat=True) @cached_property def tester_ids(self): - return self.testers.values_list('id', flat=True) + return self.testers.values_list("id", flat=True) @cached_property def usable_common_names(self): - return set(self.usable_languages.values_list('common_name', flat=True)) + return set(self.usable_languages.values_list("common_name", flat=True)) @property def usable_languages(self): - return self.allowed_languages.filter(judges__in=self.judges.filter(online=True)).distinct() + return self.allowed_languages.filter( + judges__in=self.judges.filter(online=True) + ).distinct() def translated_name(self, language): if language in self._translated_name_cache: return self._translated_name_cache[language] # Hits database despite prefetch_related. try: - name = self.translations.filter(language=language).values_list('name', flat=True)[0] + name = self.translations.filter(language=language).values_list( + "name", flat=True + )[0] except IndexError: name = self.name self._translated_name_cache[language] = name @@ -310,12 +480,23 @@ class Problem(models.Model): return ProblemClarification.objects.filter(problem=self) def update_stats(self): - self.user_count = self.submission_set.filter(points__gte=self.points, result='AC', - user__is_unlisted=False).values('user').distinct().count() + self.user_count = ( + self.submission_set.filter( + points__gte=self.points, result="AC", user__is_unlisted=False + ) + .values("user") + .distinct() + .count() + ) submissions = self.submission_set.count() if submissions: - self.ac_rate = 100.0 * self.submission_set.filter(points__gte=self.points, result='AC', - user__is_unlisted=False).count() / submissions + self.ac_rate = ( + 100.0 + * self.submission_set.filter( + points__gte=self.points, result="AC", user__is_unlisted=False + ).count() + / submissions + ) else: self.ac_rate = 0 self.save() @@ -324,9 +505,13 @@ class Problem(models.Model): def _get_limits(self, key): global_limit = getattr(self, key) - limits = {limit['language_id']: (limit['language__name'], limit[key]) - for limit in self.language_limits.values('language_id', 'language__name', key) - if limit[key] != global_limit} + limits = { + limit["language_id"]: (limit["language__name"], limit[key]) + for limit in self.language_limits.values( + "language_id", "language__name", key + ) + if limit[key] != global_limit + } limit_ids = set(limits.keys()) common = [] @@ -346,21 +531,21 @@ class Problem(models.Model): @property def language_time_limit(self): - key = 'problem_tls:%d' % self.id + key = "problem_tls:%d" % self.id result = cache.get(key) if result is not None: return result - result = self._get_limits('time_limit') + result = self._get_limits("time_limit") cache.set(key, result) return result @property def language_memory_limit(self): - key = 'problem_mls:%d' % self.id + key = "problem_mls:%d" % self.id result = cache.get(key) if result is not None: return result - result = self._get_limits('memory_limit') + result = self._get_limits("memory_limit") cache.set(key, result) return result @@ -395,105 +580,143 @@ class Problem(models.Model): return False # If the user has a full AC submission to the problem (solved the problem). - return self.submission_set.filter(user=user.profile, result='AC', points=F('problem__points')).exists() + return self.submission_set.filter( + user=user.profile, result="AC", points=F("problem__points") + ).exists() class Meta: permissions = ( - ('see_private_problem', 'See hidden problems'), - ('edit_own_problem', 'Edit own problems'), - ('edit_all_problem', 'Edit all problems'), - ('edit_public_problem', 'Edit all public problems'), - ('clone_problem', 'Clone problem'), - ('change_public_visibility', 'Change is_public field'), - ('change_manually_managed', 'Change is_manually_managed field'), - ('see_organization_problem', 'See organization-private problems'), - ('suggest_problem_changes', 'Suggest changes to problem'), + ("see_private_problem", "See hidden problems"), + ("edit_own_problem", "Edit own problems"), + ("edit_all_problem", "Edit all problems"), + ("edit_public_problem", "Edit all public problems"), + ("clone_problem", "Clone problem"), + ("change_public_visibility", "Change is_public field"), + ("change_manually_managed", "Change is_manually_managed field"), + ("see_organization_problem", "See organization-private problems"), + ("suggest_problem_changes", "Suggest changes to problem"), ) - verbose_name = _('problem') - verbose_name_plural = _('problems') + verbose_name = _("problem") + verbose_name_plural = _("problems") class ProblemTranslation(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='translations', on_delete=CASCADE) - language = models.CharField(verbose_name=_('language'), max_length=7, choices=settings.LANGUAGES) - name = models.CharField(verbose_name=_('translated name'), max_length=100, db_index=True) - description = models.TextField(verbose_name=_('translated description')) + problem = models.ForeignKey( + Problem, + verbose_name=_("problem"), + related_name="translations", + on_delete=CASCADE, + ) + language = models.CharField( + verbose_name=_("language"), max_length=7, choices=settings.LANGUAGES + ) + name = models.CharField( + verbose_name=_("translated name"), max_length=100, db_index=True + ) + description = models.TextField(verbose_name=_("translated description")) class Meta: - unique_together = ('problem', 'language') - verbose_name = _('problem translation') - verbose_name_plural = _('problem translations') + unique_together = ("problem", "language") + verbose_name = _("problem translation") + verbose_name_plural = _("problem translations") class ProblemClarification(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('clarified problem'), on_delete=CASCADE) - description = models.TextField(verbose_name=_('clarification body')) - date = models.DateTimeField(verbose_name=_('clarification timestamp'), auto_now_add=True) + problem = models.ForeignKey( + Problem, verbose_name=_("clarified problem"), on_delete=CASCADE + ) + description = models.TextField(verbose_name=_("clarification body")) + date = models.DateTimeField( + verbose_name=_("clarification timestamp"), auto_now_add=True + ) class LanguageLimit(models.Model): - problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE) - language = models.ForeignKey(Language, verbose_name=_('language'), on_delete=CASCADE) - time_limit = models.FloatField(verbose_name=_('time limit'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)]) - memory_limit = models.IntegerField(verbose_name=_('memory limit'), - validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), - MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)]) + problem = models.ForeignKey( + Problem, + verbose_name=_("problem"), + related_name="language_limits", + on_delete=CASCADE, + ) + language = models.ForeignKey( + Language, verbose_name=_("language"), on_delete=CASCADE + ) + time_limit = models.FloatField( + verbose_name=_("time limit"), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT), + ], + ) + memory_limit = models.IntegerField( + verbose_name=_("memory limit"), + validators=[ + MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT), + MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT), + ], + ) class Meta: - unique_together = ('problem', 'language') - verbose_name = _('language-specific resource limit') - verbose_name_plural = _('language-specific resource limits') + unique_together = ("problem", "language") + verbose_name = _("language-specific resource limit") + verbose_name_plural = _("language-specific resource limits") class Solution(models.Model): - problem = models.OneToOneField(Problem, on_delete=SET_NULL, verbose_name=_('associated problem'), - null=True, blank=True, related_name='solution') - is_public = models.BooleanField(verbose_name=_('public visibility'), default=False) - publish_on = models.DateTimeField(verbose_name=_('publish date')) - authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True) - content = models.TextField(verbose_name=_('editorial content')) + problem = models.OneToOneField( + Problem, + on_delete=SET_NULL, + verbose_name=_("associated problem"), + null=True, + blank=True, + related_name="solution", + ) + is_public = models.BooleanField(verbose_name=_("public visibility"), default=False) + publish_on = models.DateTimeField(verbose_name=_("publish date")) + authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True) + content = models.TextField(verbose_name=_("editorial content")) def get_absolute_url(self): problem = self.problem if problem is None: - return reverse('home') + return reverse("home") else: - return reverse('problem_editorial', args=[problem.code]) + return reverse("problem_editorial", args=[problem.code]) def __str__(self): - return _('Editorial for %s') % self.problem.name + return _("Editorial for %s") % self.problem.name class Meta: - permissions = ( - ('see_private_solution', 'See hidden solutions'), - ) - verbose_name = _('solution') - verbose_name_plural = _('solutions') + permissions = (("see_private_solution", "See hidden solutions"),) + verbose_name = _("solution") + verbose_name_plural = _("solutions") class ProblemPointsVote(models.Model): points = models.IntegerField( - verbose_name=_('proposed point value'), - help_text=_('The amount of points you think this problem deserves.'), + verbose_name=_("proposed point value"), + help_text=_("The amount of points you think this problem deserves."), validators=[ MinValueValidator(100), MaxValueValidator(600), ], ) - voter = models.ForeignKey(Profile, related_name='problem_points_votes', on_delete=CASCADE, db_index=True) - problem = models.ForeignKey(Problem, related_name='problem_points_votes', on_delete=CASCADE, db_index=True) + voter = models.ForeignKey( + Profile, related_name="problem_points_votes", on_delete=CASCADE, db_index=True + ) + problem = models.ForeignKey( + Problem, related_name="problem_points_votes", on_delete=CASCADE, db_index=True + ) vote_time = models.DateTimeField( - verbose_name=_('The time this vote was cast'), + verbose_name=_("The time this vote was cast"), auto_now_add=True, blank=True, ) class Meta: - verbose_name = _('vote') - verbose_name_plural = _('votes') + verbose_name = _("vote") + verbose_name_plural = _("votes") def __str__(self): - return f'{self.voter}: {self.points} for {self.problem.code}' \ No newline at end of file + return f"{self.voter}: {self.points} for {self.problem.code}" diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py index 3735552..8617a61 100644 --- a/judge/models/problem_data.py +++ b/judge/models/problem_data.py @@ -9,7 +9,13 @@ from django.utils.translation import gettext_lazy as _ from judge.utils.problem_data import ProblemDataStorage, get_file_cachekey -__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS'] +__all__ = [ + "problem_data_storage", + "problem_directory_file", + "ProblemData", + "ProblemTestCase", + "CHECKERS", +] problem_data_storage = ProblemDataStorage() @@ -23,52 +29,83 @@ def problem_directory_file(data, filename): CHECKERS = ( - ('standard', _('Standard')), - ('floats', _('Floats')), - ('floatsabs', _('Floats (absolute)')), - ('floatsrel', _('Floats (relative)')), - ('rstripped', _('Non-trailing spaces')), - ('sorted', _('Unordered')), - ('identical', _('Byte identical')), - ('linecount', _('Line-by-line')), - ('custom', _('Custom checker (PY)')), - ('customval', _('Custom validator (CPP)')), - ('interact', _('Interactive')), + ("standard", _("Standard")), + ("floats", _("Floats")), + ("floatsabs", _("Floats (absolute)")), + ("floatsrel", _("Floats (relative)")), + ("rstripped", _("Non-trailing spaces")), + ("sorted", _("Unordered")), + ("identical", _("Byte identical")), + ("linecount", _("Line-by-line")), + ("custom", _("Custom checker (PY)")), + ("customval", _("Custom validator (CPP)")), + ("interact", _("Interactive")), ) class ProblemData(models.Model): - problem = models.OneToOneField('Problem', verbose_name=_('problem'), related_name='data_files', - on_delete=models.CASCADE) - zipfile = models.FileField(verbose_name=_('data zip file'), storage=problem_data_storage, null=True, blank=True, - upload_to=problem_directory_file) - generator = models.FileField(verbose_name=_('generator file'), storage=problem_data_storage, null=True, blank=True, - upload_to=problem_directory_file) - output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True) - output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True) - feedback = models.TextField(verbose_name=_('init.yml generation feedback'), blank=True) - checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True) - checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True, - help_text=_('checker arguments as a JSON object')) - custom_checker = models.FileField(verbose_name=_('custom checker file'), - storage=problem_data_storage, - null=True, - blank=True, - upload_to=problem_directory_file, - validators=[FileExtensionValidator(allowed_extensions=['py'])]) - custom_validator = models.FileField(verbose_name=_('custom validator file'), - storage=problem_data_storage, - null=True, - blank=True, - upload_to=problem_directory_file, - validators=[FileExtensionValidator(allowed_extensions=['cpp'])]) - interactive_judge = models.FileField(verbose_name=_('interactive judge'), - storage=problem_data_storage, - null=True, - blank=True, - upload_to=problem_directory_file, - validators=[FileExtensionValidator(allowed_extensions=['cpp'])]) - + problem = models.OneToOneField( + "Problem", + verbose_name=_("problem"), + related_name="data_files", + on_delete=models.CASCADE, + ) + zipfile = models.FileField( + verbose_name=_("data zip file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + ) + generator = models.FileField( + verbose_name=_("generator file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + ) + output_prefix = models.IntegerField( + verbose_name=_("output prefix length"), blank=True, null=True + ) + output_limit = models.IntegerField( + verbose_name=_("output limit length"), blank=True, null=True + ) + feedback = models.TextField( + verbose_name=_("init.yml generation feedback"), blank=True + ) + checker = models.CharField( + max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True + ) + checker_args = models.TextField( + verbose_name=_("checker arguments"), + blank=True, + help_text=_("checker arguments as a JSON object"), + ) + custom_checker = models.FileField( + verbose_name=_("custom checker file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + validators=[FileExtensionValidator(allowed_extensions=["py"])], + ) + custom_validator = models.FileField( + verbose_name=_("custom validator file"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + validators=[FileExtensionValidator(allowed_extensions=["cpp"])], + ) + interactive_judge = models.FileField( + verbose_name=_("interactive judge"), + storage=problem_data_storage, + null=True, + blank=True, + upload_to=problem_directory_file, + validators=[FileExtensionValidator(allowed_extensions=["cpp"])], + ) + __original_zipfile = None def __init__(self, *args, **kwargs): @@ -78,10 +115,13 @@ class ProblemData(models.Model): def save(self, *args, **kwargs): # Delete caches if self.__original_zipfile: - try: + 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_key = "problem_archive:%s:%s" % ( + self.problem.code, + get_file_cachekey(file), + ) cache.delete(cache_key) except BadZipFile: pass @@ -90,7 +130,7 @@ class ProblemData(models.Model): return super(ProblemData, self).save(*args, **kwargs) def has_yml(self): - return problem_data_storage.exists('%s/init.yml' % self.problem.code) + return problem_data_storage.exists("%s/init.yml" % self.problem.code) def _update_code(self, original, new): try: @@ -103,31 +143,60 @@ class ProblemData(models.Model): if self.generator: self.generator.name = _problem_directory_file(new, self.generator.name) if self.custom_checker: - self.custom_checker.name = _problem_directory_file(new, self.custom_checker.name) + self.custom_checker.name = _problem_directory_file( + new, self.custom_checker.name + ) if self.custom_checker: - self.custom_checker.name = _problem_directory_file(new, self.custom_checker.name) + self.custom_checker.name = _problem_directory_file( + new, self.custom_checker.name + ) if self.custom_validator: - self.custom_validator.name = _problem_directory_file(new, self.custom_validator.name) + self.custom_validator.name = _problem_directory_file( + new, self.custom_validator.name + ) self.save() + _update_code.alters_data = True class ProblemTestCase(models.Model): - dataset = models.ForeignKey('Problem', verbose_name=_('problem data set'), related_name='cases', - on_delete=models.CASCADE) - order = models.IntegerField(verbose_name=_('case position')) - type = models.CharField(max_length=1, verbose_name=_('case type'), - choices=(('C', _('Normal case')), - ('S', _('Batch start')), - ('E', _('Batch end'))), - default='C') - input_file = models.CharField(max_length=100, verbose_name=_('input file name'), blank=True) - output_file = models.CharField(max_length=100, verbose_name=_('output file name'), blank=True) - generator_args = models.TextField(verbose_name=_('generator arguments'), blank=True) - points = models.IntegerField(verbose_name=_('point value'), blank=True, null=True) - is_pretest = models.BooleanField(verbose_name=_('case is pretest?')) - output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True) - output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True) - checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True) - checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True, - help_text=_('checker arguments as a JSON object')) + dataset = models.ForeignKey( + "Problem", + verbose_name=_("problem data set"), + related_name="cases", + on_delete=models.CASCADE, + ) + order = models.IntegerField(verbose_name=_("case position")) + type = models.CharField( + max_length=1, + verbose_name=_("case type"), + choices=( + ("C", _("Normal case")), + ("S", _("Batch start")), + ("E", _("Batch end")), + ), + default="C", + ) + input_file = models.CharField( + max_length=100, verbose_name=_("input file name"), blank=True + ) + output_file = models.CharField( + max_length=100, verbose_name=_("output file name"), blank=True + ) + generator_args = models.TextField(verbose_name=_("generator arguments"), blank=True) + points = models.IntegerField(verbose_name=_("point value"), blank=True, null=True) + is_pretest = models.BooleanField(verbose_name=_("case is pretest?")) + output_prefix = models.IntegerField( + verbose_name=_("output prefix length"), blank=True, null=True + ) + output_limit = models.IntegerField( + verbose_name=_("output limit length"), blank=True, null=True + ) + checker = models.CharField( + max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True + ) + checker_args = models.TextField( + verbose_name=_("checker arguments"), + blank=True, + help_text=_("checker arguments as a JSON object"), + ) diff --git a/judge/models/profile.py b/judge/models/profile.py index b00dc50..e155a20 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -16,7 +16,7 @@ from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.runtime import Language from judge.ratings import rating_class -__all__ = ['Organization', 'Profile', 'OrganizationRequest', 'Friend'] +__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"] class EncryptedNullCharField(EncryptedCharField): @@ -27,28 +27,65 @@ class EncryptedNullCharField(EncryptedCharField): class Organization(models.Model): - name = models.CharField(max_length=128, verbose_name=_('organization title')) - slug = models.SlugField(max_length=128, verbose_name=_('organization slug'), - help_text=_('Organization name shown in URL')) - short_name = models.CharField(max_length=20, verbose_name=_('short name'), - help_text=_('Displayed beside user name during contests')) - about = models.TextField(verbose_name=_('organization description')) - registrant = models.ForeignKey('Profile', verbose_name=_('registrant'), on_delete=models.CASCADE, - related_name='registrant+', help_text=_('User who registered this organization')) - admins = models.ManyToManyField('Profile', verbose_name=_('administrators'), related_name='admin_of', - help_text=_('Those who can edit this organization')) - creation_date = models.DateTimeField(verbose_name=_('creation date'), auto_now_add=True) - is_open = models.BooleanField(verbose_name=_('is open organization?'), - help_text=_('Allow joining organization'), default=True) - slots = models.IntegerField(verbose_name=_('maximum size'), null=True, blank=True, - help_text=_('Maximum amount of users in this organization, ' - 'only applicable to private organizations')) - access_code = models.CharField(max_length=7, help_text=_('Student access code'), - verbose_name=_('access code'), null=True, blank=True) - logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150, - blank=True, - help_text=_('This image will replace the default site logo for users ' - 'viewing the organization.')) + name = models.CharField(max_length=128, verbose_name=_("organization title")) + slug = models.SlugField( + max_length=128, + verbose_name=_("organization slug"), + help_text=_("Organization name shown in URL"), + ) + short_name = models.CharField( + max_length=20, + verbose_name=_("short name"), + help_text=_("Displayed beside user name during contests"), + ) + about = models.TextField(verbose_name=_("organization description")) + registrant = models.ForeignKey( + "Profile", + verbose_name=_("registrant"), + on_delete=models.CASCADE, + related_name="registrant+", + help_text=_("User who registered this organization"), + ) + admins = models.ManyToManyField( + "Profile", + verbose_name=_("administrators"), + related_name="admin_of", + help_text=_("Those who can edit this organization"), + ) + creation_date = models.DateTimeField( + verbose_name=_("creation date"), auto_now_add=True + ) + is_open = models.BooleanField( + verbose_name=_("is open organization?"), + help_text=_("Allow joining organization"), + default=True, + ) + slots = models.IntegerField( + verbose_name=_("maximum size"), + null=True, + blank=True, + help_text=_( + "Maximum amount of users in this organization, " + "only applicable to private organizations" + ), + ) + access_code = models.CharField( + max_length=7, + help_text=_("Student access code"), + verbose_name=_("access code"), + null=True, + blank=True, + ) + logo_override_image = models.CharField( + verbose_name=_("Logo override image"), + default="", + max_length=150, + blank=True, + help_text=_( + "This image will replace the default site logo for users " + "viewing the organization." + ), + ) def __contains__(self, item): if isinstance(item, int): @@ -56,69 +93,128 @@ class Organization(models.Model): elif isinstance(item, Profile): return self.members.filter(id=item.id).exists() else: - raise TypeError('Organization membership test must be Profile or primany key') + raise TypeError( + "Organization membership test must be Profile or primany key" + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('organization_home', args=(self.id, self.slug)) + return reverse("organization_home", args=(self.id, self.slug)) def get_users_url(self): - return reverse('organization_users', args=(self.id, self.slug)) + return reverse("organization_users", args=(self.id, self.slug)) class Meta: - ordering = ['name'] + ordering = ["name"] permissions = ( - ('organization_admin', 'Administer organizations'), - ('edit_all_organization', 'Edit all organizations'), + ("organization_admin", "Administer organizations"), + ("edit_all_organization", "Edit all organizations"), ) - verbose_name = _('organization') - verbose_name_plural = _('organizations') + verbose_name = _("organization") + verbose_name_plural = _("organizations") class Profile(models.Model): - user = models.OneToOneField(User, verbose_name=_('user associated'), on_delete=models.CASCADE) - about = models.TextField(verbose_name=_('self-description'), null=True, blank=True) - timezone = models.CharField(max_length=50, verbose_name=_('location'), choices=TIMEZONE, - default=settings.DEFAULT_USER_TIME_ZONE) - language = models.ForeignKey('Language', verbose_name=_('preferred language'), on_delete=models.SET_DEFAULT, - default=Language.get_default_language_pk) + user = models.OneToOneField( + User, verbose_name=_("user associated"), on_delete=models.CASCADE + ) + about = models.TextField(verbose_name=_("self-description"), null=True, blank=True) + timezone = models.CharField( + max_length=50, + verbose_name=_("location"), + choices=TIMEZONE, + default=settings.DEFAULT_USER_TIME_ZONE, + ) + language = models.ForeignKey( + "Language", + verbose_name=_("preferred language"), + on_delete=models.SET_DEFAULT, + default=Language.get_default_language_pk, + ) points = models.FloatField(default=0, db_index=True) performance_points = models.FloatField(default=0, db_index=True) problem_count = models.IntegerField(default=0, db_index=True) - ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default='github') - last_access = models.DateTimeField(verbose_name=_('last access time'), default=now) - ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True) - organizations = SortedManyToManyField(Organization, verbose_name=_('organization'), blank=True, - related_name='members', related_query_name='member') - display_rank = models.CharField(max_length=10, default='user', verbose_name=_('display rank'), - choices=(('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin'))) - mute = models.BooleanField(verbose_name=_('comment mute'), help_text=_('Some users are at their best when silent.'), - default=False) - is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'), - default=False) + ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default="github") + last_access = models.DateTimeField(verbose_name=_("last access time"), default=now) + ip = models.GenericIPAddressField(verbose_name=_("last IP"), blank=True, null=True) + organizations = SortedManyToManyField( + Organization, + verbose_name=_("organization"), + blank=True, + related_name="members", + related_query_name="member", + ) + display_rank = models.CharField( + max_length=10, + default="user", + verbose_name=_("display rank"), + choices=( + ("user", "Normal User"), + ("setter", "Problem Setter"), + ("admin", "Admin"), + ), + ) + mute = models.BooleanField( + verbose_name=_("comment mute"), + help_text=_("Some users are at their best when silent."), + default=False, + ) + is_unlisted = models.BooleanField( + verbose_name=_("unlisted user"), + help_text=_("User will not be ranked."), + default=False, + ) is_banned_problem_voting = models.BooleanField( - verbose_name=_('banned from voting'), + verbose_name=_("banned from voting"), help_text=_("User will not be able to vote on problems' point values."), default=False, ) rating = models.IntegerField(null=True, default=None) - user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536, - help_text=_('User-defined JavaScript for site customization.')) - current_contest = models.OneToOneField('ContestParticipation', verbose_name=_('current contest'), - null=True, blank=True, related_name='+', on_delete=models.SET_NULL) - math_engine = models.CharField(verbose_name=_('math engine'), choices=MATH_ENGINES_CHOICES, max_length=4, - default=settings.MATHOID_DEFAULT_TYPE, - help_text=_('the rendering engine used to render math')) - is_totp_enabled = models.BooleanField(verbose_name=_('2FA enabled'), default=False, - help_text=_('check to enable TOTP-based two factor authentication')) - totp_key = EncryptedNullCharField(max_length=32, null=True, blank=True, verbose_name=_('TOTP key'), - help_text=_('32 character base32-encoded key for TOTP'), - validators=[RegexValidator('^$|^[A-Z2-7]{32}$', - _('TOTP key must be empty or base32'))]) - notes = models.TextField(verbose_name=_('internal notes'), null=True, blank=True, - help_text=_('Notes for administrators regarding this user.')) + user_script = models.TextField( + verbose_name=_("user script"), + default="", + blank=True, + max_length=65536, + help_text=_("User-defined JavaScript for site customization."), + ) + current_contest = models.OneToOneField( + "ContestParticipation", + verbose_name=_("current contest"), + null=True, + blank=True, + related_name="+", + on_delete=models.SET_NULL, + ) + math_engine = models.CharField( + verbose_name=_("math engine"), + choices=MATH_ENGINES_CHOICES, + max_length=4, + default=settings.MATHOID_DEFAULT_TYPE, + help_text=_("the rendering engine used to render math"), + ) + is_totp_enabled = models.BooleanField( + verbose_name=_("2FA enabled"), + default=False, + help_text=_("check to enable TOTP-based two factor authentication"), + ) + totp_key = EncryptedNullCharField( + max_length=32, + null=True, + blank=True, + verbose_name=_("TOTP key"), + help_text=_("32 character base32-encoded key for TOTP"), + validators=[ + RegexValidator("^$|^[A-Z2-7]{32}$", _("TOTP key must be empty or base32")) + ], + ) + notes = models.TextField( + verbose_name=_("internal notes"), + null=True, + blank=True, + help_text=_("Notes for administrators regarding this user."), + ) @cached_property def organization(self): @@ -133,33 +229,45 @@ class Profile(models.Model): @cached_property def count_unseen_notifications(self): query = { - 'read': False, + "read": False, } return self.notifications.filter(**query).count() - + _pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] def calculate_points(self, table=_pp_table): from judge.models import Problem + 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) + 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() + 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) entries = min(len(data), len(table)) pp = sum(map(mul, table[:entries], data[:entries])) + bonus_function(extradata) - if self.points != points or problems != self.problem_count or self.performance_points != pp: + if ( + self.points != points + or problems != self.problem_count + or self.performance_points != pp + ): self.points = points self.problem_count = problems self.performance_points = pp - self.save(update_fields=['points', 'problem_count', 'performance_points']) + self.save(update_fields=["points", "problem_count", "performance_points"]) return points calculate_points.alters_data = True @@ -172,9 +280,12 @@ class Profile(models.Model): def update_contest(self): 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)): + 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() @@ -182,86 +293,105 @@ class Profile(models.Model): update_contest.alters_data = True def get_absolute_url(self): - return reverse('user_page', args=(self.user.username,)) + return reverse("user_page", args=(self.user.username,)) def __str__(self): return self.user.username @classmethod - def get_user_css_class(cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS): + def get_user_css_class( + cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS + ): if rating_colors: - return 'rating %s %s' % (rating_class(rating) if rating is not None else 'rate-none', display_rank) + return "rating %s %s" % ( + rating_class(rating) if rating is not None else "rate-none", + display_rank, + ) return display_rank @cached_property def css_class(self): return self.get_user_css_class(self.display_rank, self.rating) - def get_friends(self): #list of usernames, including you + def get_friends(self): # list of usernames, including you friend_obj = self.following_users.all() ret = set() - if (friend_obj): + if friend_obj: ret = set(friend.username for friend in friend_obj[0].users.all()) - + ret.add(self.username) return ret class Meta: permissions = ( - ('test_site', 'Shows in-progress development stuff'), - ('totp', 'Edit TOTP settings'), + ("test_site", "Shows in-progress development stuff"), + ("totp", "Edit TOTP settings"), ) - verbose_name = _('user profile') - verbose_name_plural = _('user profiles') + verbose_name = _("user profile") + verbose_name_plural = _("user profiles") class OrganizationRequest(models.Model): - user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='requests', on_delete=models.CASCADE) - organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='requests', - on_delete=models.CASCADE) - time = models.DateTimeField(verbose_name=_('request time'), auto_now_add=True) - state = models.CharField(max_length=1, verbose_name=_('state'), choices=( - ('P', 'Pending'), - ('A', 'Approved'), - ('R', 'Rejected'), - )) - reason = models.TextField(verbose_name=_('reason')) + user = models.ForeignKey( + Profile, + verbose_name=_("user"), + related_name="requests", + on_delete=models.CASCADE, + ) + organization = models.ForeignKey( + Organization, + verbose_name=_("organization"), + related_name="requests", + on_delete=models.CASCADE, + ) + time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True) + state = models.CharField( + max_length=1, + verbose_name=_("state"), + choices=( + ("P", "Pending"), + ("A", "Approved"), + ("R", "Rejected"), + ), + ) + reason = models.TextField(verbose_name=_("reason")) class Meta: - verbose_name = _('organization join request') - verbose_name_plural = _('organization join requests') + verbose_name = _("organization join request") + verbose_name_plural = _("organization join requests") class Friend(models.Model): users = models.ManyToManyField(Profile) - current_user = models.ForeignKey(Profile, related_name="following_users", on_delete=CASCADE) + current_user = models.ForeignKey( + Profile, related_name="following_users", on_delete=CASCADE + ) @classmethod def is_friend(self, current_user, new_friend): try: - return current_user.following_users.get().users \ - .filter(user=new_friend.user).exists() + return ( + current_user.following_users.get() + .users.filter(user=new_friend.user) + .exists() + ) except: return False @classmethod def make_friend(self, current_user, new_friend): - friend, created = self.objects.get_or_create( - current_user = current_user - ) + friend, created = self.objects.get_or_create(current_user=current_user) friend.users.add(new_friend) @classmethod def remove_friend(self, current_user, new_friend): - friend, created = self.objects.get_or_create( - current_user = current_user - ) + friend, created = self.objects.get_or_create(current_user=current_user) friend.users.remove(new_friend) @classmethod def toggle_friend(self, current_user, new_friend): - if (self.is_friend(current_user, new_friend)): + if self.is_friend(current_user, new_friend): self.remove_friend(current_user, new_friend) else: self.make_friend(current_user, new_friend) diff --git a/judge/models/runtime.py b/judge/models/runtime.py index 16cdd66..99825ec 100644 --- a/judge/models/runtime.py +++ b/judge/models/runtime.py @@ -12,38 +12,82 @@ from django.utils.translation import gettext_lazy as _ from judge.judgeapi import disconnect_judge -__all__ = ['Language', 'RuntimeVersion', 'Judge'] +__all__ = ["Language", "RuntimeVersion", "Judge"] class Language(models.Model): - key = models.CharField(max_length=6, verbose_name=_('short identifier'), - help_text=_('The identifier for this language; the same as its executor id for judges.'), - unique=True) - name = models.CharField(max_length=20, verbose_name=_('long name'), - help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".')) - short_name = models.CharField(max_length=10, verbose_name=_('short name'), - help_text=_('More readable, but short, name to display publicly; e.g. "PY2" or ' - '"C++11". If left blank, it will default to the ' - 'short identifier.'), - null=True, blank=True) - common_name = models.CharField(max_length=10, verbose_name=_('common name'), - help_text=_('Common name for the language. For example, the common name for C++03, ' - 'C++11, and C++14 would be "C++"')) - ace = models.CharField(max_length=20, verbose_name=_('ace mode name'), - help_text=_('Language ID for Ace.js editor highlighting, appended to "mode-" to determine ' - 'the Ace JavaScript file to use, e.g., "python".')) - pygments = models.CharField(max_length=20, verbose_name=_('pygments name'), - help_text=_('Language ID for Pygments highlighting in source windows.')) - template = models.TextField(verbose_name=_('code template'), - help_text=_('Code template to display in submission editor.'), blank=True) - info = models.CharField(max_length=50, verbose_name=_('runtime info override'), blank=True, - help_text=_("Do not set this unless you know what you're doing! It will override the " - "usually more specific, judge-provided runtime info!")) - description = models.TextField(verbose_name=_('language description'), - help_text=_('Use this field to inform users of quirks with your environment, ' - 'additional restrictions, etc.'), blank=True) - extension = models.CharField(max_length=10, verbose_name=_('extension'), - help_text=_('The extension of source files, e.g., "py" or "cpp".')) + key = models.CharField( + max_length=6, + verbose_name=_("short identifier"), + help_text=_( + "The identifier for this language; the same as its executor id for judges." + ), + unique=True, + ) + name = models.CharField( + max_length=20, + verbose_name=_("long name"), + help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'), + ) + short_name = models.CharField( + max_length=10, + verbose_name=_("short name"), + help_text=_( + 'More readable, but short, name to display publicly; e.g. "PY2" or ' + '"C++11". If left blank, it will default to the ' + "short identifier." + ), + null=True, + blank=True, + ) + common_name = models.CharField( + max_length=10, + verbose_name=_("common name"), + help_text=_( + "Common name for the language. For example, the common name for C++03, " + 'C++11, and C++14 would be "C++"' + ), + ) + ace = models.CharField( + max_length=20, + verbose_name=_("ace mode name"), + help_text=_( + 'Language ID for Ace.js editor highlighting, appended to "mode-" to determine ' + 'the Ace JavaScript file to use, e.g., "python".' + ), + ) + pygments = models.CharField( + max_length=20, + verbose_name=_("pygments name"), + help_text=_("Language ID for Pygments highlighting in source windows."), + ) + template = models.TextField( + verbose_name=_("code template"), + help_text=_("Code template to display in submission editor."), + blank=True, + ) + info = models.CharField( + max_length=50, + verbose_name=_("runtime info override"), + blank=True, + help_text=_( + "Do not set this unless you know what you're doing! It will override the " + "usually more specific, judge-provided runtime info!" + ), + ) + description = models.TextField( + verbose_name=_("language description"), + help_text=_( + "Use this field to inform users of quirks with your environment, " + "additional restrictions, etc." + ), + blank=True, + ) + extension = models.CharField( + max_length=10, + verbose_name=_("extension"), + help_text=_('The extension of source files, e.g., "py" or "cpp".'), + ) def runtime_versions(self): runtimes = OrderedDict() @@ -52,25 +96,29 @@ class Language(models.Model): id = runtime.name if id not in runtimes: runtimes[id] = set() - if not runtime.version: # empty str == error determining version on judge side + if ( + not runtime.version + ): # empty str == error determining version on judge side continue runtimes[id].add(runtime.version) lang_versions = [] for id, version_list in runtimes.items(): - lang_versions.append((id, sorted(version_list, key=lambda a: tuple(map(int, a.split('.')))))) + lang_versions.append( + (id, sorted(version_list, key=lambda a: tuple(map(int, a.split("."))))) + ) return lang_versions @classmethod def get_common_name_map(cls): - result = cache.get('lang:cn_map') + result = cache.get("lang:cn_map") if result is not None: return result result = defaultdict(set) - for id, cn in Language.objects.values_list('id', 'common_name'): + for id, cn in Language.objects.values_list("id", "common_name"): result[cn].add(id) result = {id: cns for id, cns in result.items() if len(cns) > 1} - cache.set('lang:cn_map', result, 86400) + cache.set("lang:cn_map", result, 86400) return result @cached_property @@ -83,17 +131,19 @@ class Language(models.Model): @cached_property def display_name(self): if self.info: - return '%s (%s)' % (self.name, self.info) + return "%s (%s)" % (self.name, self.info) else: return self.name @classmethod def get_python3(cls): # We really need a default language, and this app is in Python 3 - return Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0] + return Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[ + 0 + ] def get_absolute_url(self): - return reverse('runtime_list') + '#' + self.key + return reverse("runtime_list") + "#" + self.key @classmethod def get_default_language(cls): @@ -107,36 +157,67 @@ class Language(models.Model): return cls.get_default_language().pk class Meta: - ordering = ['key'] - verbose_name = _('language') - verbose_name_plural = _('languages') + ordering = ["key"] + verbose_name = _("language") + verbose_name_plural = _("languages") class RuntimeVersion(models.Model): - language = models.ForeignKey(Language, verbose_name=_('language to which this runtime belongs'), on_delete=CASCADE) - judge = models.ForeignKey('Judge', verbose_name=_('judge on which this runtime exists'), on_delete=CASCADE) - name = models.CharField(max_length=64, verbose_name=_('runtime name')) - version = models.CharField(max_length=64, verbose_name=_('runtime version'), blank=True) - priority = models.IntegerField(verbose_name=_('order in which to display this runtime'), default=0) + language = models.ForeignKey( + Language, + verbose_name=_("language to which this runtime belongs"), + on_delete=CASCADE, + ) + judge = models.ForeignKey( + "Judge", verbose_name=_("judge on which this runtime exists"), on_delete=CASCADE + ) + name = models.CharField(max_length=64, verbose_name=_("runtime name")) + version = models.CharField( + max_length=64, verbose_name=_("runtime version"), blank=True + ) + priority = models.IntegerField( + verbose_name=_("order in which to display this runtime"), default=0 + ) class Judge(models.Model): - name = models.CharField(max_length=50, help_text=_('Server name, hostname-style'), unique=True) - created = models.DateTimeField(auto_now_add=True, verbose_name=_('time of creation')) - auth_key = models.CharField(max_length=100, help_text=_('A key to authenticate this judge'), - verbose_name=_('authentication key')) - is_blocked = models.BooleanField(verbose_name=_('block judge'), default=False, - help_text=_('Whether this judge should be blocked from connecting, ' - 'even if its key is correct.')) - online = models.BooleanField(verbose_name=_('judge online status'), default=False) - start_time = models.DateTimeField(verbose_name=_('judge start time'), null=True) - ping = models.FloatField(verbose_name=_('response time'), null=True) - load = models.FloatField(verbose_name=_('system load'), null=True, - help_text=_('Load for the last minute, divided by processors to be fair.')) - description = models.TextField(blank=True, verbose_name=_('description')) - last_ip = models.GenericIPAddressField(verbose_name='Last connected IP', blank=True, null=True) - problems = models.ManyToManyField('Problem', verbose_name=_('problems'), related_name='judges') - runtimes = models.ManyToManyField(Language, verbose_name=_('judges'), related_name='judges') + name = models.CharField( + max_length=50, help_text=_("Server name, hostname-style"), unique=True + ) + created = models.DateTimeField( + auto_now_add=True, verbose_name=_("time of creation") + ) + auth_key = models.CharField( + max_length=100, + help_text=_("A key to authenticate this judge"), + verbose_name=_("authentication key"), + ) + is_blocked = models.BooleanField( + verbose_name=_("block judge"), + default=False, + help_text=_( + "Whether this judge should be blocked from connecting, " + "even if its key is correct." + ), + ) + online = models.BooleanField(verbose_name=_("judge online status"), default=False) + start_time = models.DateTimeField(verbose_name=_("judge start time"), null=True) + ping = models.FloatField(verbose_name=_("response time"), null=True) + load = models.FloatField( + verbose_name=_("system load"), + null=True, + help_text=_("Load for the last minute, divided by processors to be fair."), + ) + description = models.TextField(blank=True, verbose_name=_("description")) + last_ip = models.GenericIPAddressField( + verbose_name="Last connected IP", blank=True, null=True + ) + problems = models.ManyToManyField( + "Problem", verbose_name=_("problems"), related_name="judges" + ) + runtimes = models.ManyToManyField( + Language, verbose_name=_("judges"), related_name="judges" + ) def __str__(self): return self.name @@ -148,22 +229,23 @@ class Judge(models.Model): @cached_property def runtime_versions(self): - qs = (self.runtimeversion_set.values('language__key', 'language__name', 'version', 'name') - .order_by('language__key', 'priority')) + qs = self.runtimeversion_set.values( + "language__key", "language__name", "version", "name" + ).order_by("language__key", "priority") ret = OrderedDict() for data in qs: - key = data['language__key'] + key = data["language__key"] if key not in ret: - ret[key] = {'name': data['language__name'], 'runtime': []} - ret[key]['runtime'].append((data['name'], (data['version'],))) + ret[key] = {"name": data["language__name"], "runtime": []} + ret[key]["runtime"].append((data["name"], (data["version"],))) return list(ret.items()) @cached_property def uptime(self): - return timezone.now() - self.start_time if self.online else 'N/A' + return timezone.now() - self.start_time if self.online else "N/A" @cached_property def ping_ms(self): @@ -171,9 +253,9 @@ class Judge(models.Model): @cached_property def runtime_list(self): - return map(attrgetter('name'), self.runtimes.all()) + return map(attrgetter("name"), self.runtimes.all()) class Meta: - ordering = ['name'] - verbose_name = _('judge') - verbose_name_plural = _('judges') + ordering = ["name"] + verbose_name = _("judge") + verbose_name_plural = _("judges") diff --git a/judge/models/submission.py b/judge/models/submission.py index e6c0208..51bb1ee 100644 --- a/judge/models/submission.py +++ b/judge/models/submission.py @@ -14,92 +14,130 @@ from judge.models.profile import Profile from judge.models.runtime import Language from judge.utils.unicode import utf8bytes -__all__ = ['SUBMISSION_RESULT', 'Submission', 'SubmissionSource', 'SubmissionTestCase'] +__all__ = ["SUBMISSION_RESULT", "Submission", "SubmissionSource", "SubmissionTestCase"] SUBMISSION_RESULT = ( - ('AC', _('Accepted')), - ('WA', _('Wrong Answer')), - ('TLE', _('Time Limit Exceeded')), - ('MLE', _('Memory Limit Exceeded')), - ('OLE', _('Output Limit Exceeded')), - ('IR', _('Invalid Return')), - ('RTE', _('Runtime Error')), - ('CE', _('Compile Error')), - ('IE', _('Internal Error')), - ('SC', _('Short circuit')), - ('AB', _('Aborted')), + ("AC", _("Accepted")), + ("WA", _("Wrong Answer")), + ("TLE", _("Time Limit Exceeded")), + ("MLE", _("Memory Limit Exceeded")), + ("OLE", _("Output Limit Exceeded")), + ("IR", _("Invalid Return")), + ("RTE", _("Runtime Error")), + ("CE", _("Compile Error")), + ("IE", _("Internal Error")), + ("SC", _("Short circuit")), + ("AB", _("Aborted")), ) class Submission(models.Model): STATUS = ( - ('QU', _('Queued')), - ('P', _('Processing')), - ('G', _('Grading')), - ('D', _('Completed')), - ('IE', _('Internal Error')), - ('CE', _('Compile Error')), - ('AB', _('Aborted')), + ("QU", _("Queued")), + ("P", _("Processing")), + ("G", _("Grading")), + ("D", _("Completed")), + ("IE", _("Internal Error")), + ("CE", _("Compile Error")), + ("AB", _("Aborted")), ) - IN_PROGRESS_GRADING_STATUS = ('QU', 'P', 'G') + IN_PROGRESS_GRADING_STATUS = ("QU", "P", "G") RESULT = SUBMISSION_RESULT USER_DISPLAY_CODES = { - 'AC': _('Accepted'), - 'WA': _('Wrong Answer'), - 'SC': "Short Circuited", - 'TLE': _('Time Limit Exceeded'), - 'MLE': _('Memory Limit Exceeded'), - 'OLE': _('Output Limit Exceeded'), - 'IR': _('Invalid Return'), - 'RTE': _('Runtime Error'), - 'CE': _('Compile Error'), - 'IE': _('Internal Error (judging server error)'), - 'QU': _('Queued'), - 'P': _('Processing'), - 'G': _('Grading'), - 'D': _('Completed'), - 'AB': _('Aborted'), + "AC": _("Accepted"), + "WA": _("Wrong Answer"), + "SC": "Short Circuited", + "TLE": _("Time Limit Exceeded"), + "MLE": _("Memory Limit Exceeded"), + "OLE": _("Output Limit Exceeded"), + "IR": _("Invalid Return"), + "RTE": _("Runtime Error"), + "CE": _("Compile Error"), + "IE": _("Internal Error (judging server error)"), + "QU": _("Queued"), + "P": _("Processing"), + "G": _("Grading"), + "D": _("Completed"), + "AB": _("Aborted"), } user = models.ForeignKey(Profile, on_delete=models.CASCADE) problem = models.ForeignKey(Problem, on_delete=models.CASCADE) - date = models.DateTimeField(verbose_name=_('submission time'), auto_now_add=True, db_index=True) - time = models.FloatField(verbose_name=_('execution time'), null=True, db_index=True) - memory = models.FloatField(verbose_name=_('memory usage'), null=True) - points = models.FloatField(verbose_name=_('points granted'), null=True, db_index=True) - language = models.ForeignKey(Language, verbose_name=_('submission language'), on_delete=models.CASCADE) - status = models.CharField(verbose_name=_('status'), max_length=2, choices=STATUS, default='QU', db_index=True) - result = models.CharField(verbose_name=_('result'), max_length=3, choices=SUBMISSION_RESULT, - default=None, null=True, blank=True, db_index=True) - error = models.TextField(verbose_name=_('compile errors'), null=True, blank=True) + date = models.DateTimeField( + verbose_name=_("submission time"), auto_now_add=True, db_index=True + ) + time = models.FloatField(verbose_name=_("execution time"), null=True, db_index=True) + memory = models.FloatField(verbose_name=_("memory usage"), null=True) + points = models.FloatField( + verbose_name=_("points granted"), null=True, db_index=True + ) + language = models.ForeignKey( + Language, verbose_name=_("submission language"), on_delete=models.CASCADE + ) + status = models.CharField( + verbose_name=_("status"), + max_length=2, + choices=STATUS, + default="QU", + db_index=True, + ) + result = models.CharField( + verbose_name=_("result"), + max_length=3, + choices=SUBMISSION_RESULT, + default=None, + null=True, + blank=True, + db_index=True, + ) + error = models.TextField(verbose_name=_("compile errors"), null=True, blank=True) current_testcase = models.IntegerField(default=0) - batch = models.BooleanField(verbose_name=_('batched cases'), default=False) - case_points = models.FloatField(verbose_name=_('test case points'), default=0) - case_total = models.FloatField(verbose_name=_('test case total points'), default=0) - judged_on = models.ForeignKey('Judge', verbose_name=_('judged on'), null=True, blank=True, - on_delete=models.SET_NULL) - judged_date = models.DateTimeField(verbose_name=_('submission judge time'), default=None, null=True) - was_rejudged = models.BooleanField(verbose_name=_('was rejudged by admin'), default=False) - is_pretested = models.BooleanField(verbose_name=_('was ran on pretests only'), default=False) - contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True, - on_delete=models.SET_NULL, related_name='+') + batch = models.BooleanField(verbose_name=_("batched cases"), default=False) + case_points = models.FloatField(verbose_name=_("test case points"), default=0) + case_total = models.FloatField(verbose_name=_("test case total points"), default=0) + judged_on = models.ForeignKey( + "Judge", + verbose_name=_("judged on"), + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + judged_date = models.DateTimeField( + verbose_name=_("submission judge time"), default=None, null=True + ) + was_rejudged = models.BooleanField( + verbose_name=_("was rejudged by admin"), default=False + ) + is_pretested = models.BooleanField( + verbose_name=_("was ran on pretests only"), default=False + ) + contest_object = models.ForeignKey( + "Contest", + verbose_name=_("contest"), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) objects = TranslatedProblemForeignKeyQuerySet.as_manager() @classmethod def result_class_from_code(cls, result, case_points, case_total): - if result == 'AC': + if result == "AC": if case_points == case_total: - return 'AC' - return '_AC' + return "AC" + return "_AC" return result @property def result_class(self): # This exists to save all these conditionals from being executed (slowly) in each row.jade template - if self.status in ('IE', 'CE'): + if self.status in ("IE", "CE"): return self.status - return Submission.result_class_from_code(self.result, self.case_points, self.case_total) + return Submission.result_class_from_code( + self.result, self.case_points, self.case_total + ) @property def memory_bytes(self): @@ -111,12 +149,11 @@ class Submission(models.Model): @property def long_status(self): - return Submission.USER_DISPLAY_CODES.get(self.short_status, '') + return Submission.USER_DISPLAY_CODES.get(self.short_status, "") def judge(self, *args, **kwargs): judge_submission(self, *args, **kwargs) - judge.alters_data = True def abort(self): @@ -131,8 +168,12 @@ class Submission(models.Model): return contest_problem = contest.problem - contest.points = round(self.case_points / self.case_total * contest_problem.points - if self.case_total > 0 else 0, 3) + contest.points = round( + self.case_points / self.case_total * contest_problem.points + if self.case_total > 0 + else 0, + 3, + ) if not contest_problem.partial and contest.points != contest_problem.points: contest.points = 0 contest.save() @@ -142,18 +183,22 @@ class Submission(models.Model): @property def is_graded(self): - return self.status not in ('QU', 'P', 'G') + return self.status not in ("QU", "P", "G") @cached_property def contest_key(self): - if hasattr(self, 'contest'): + if hasattr(self, "contest"): return self.contest_object.key def __str__(self): - return 'Submission %d of %s by %s' % (self.id, self.problem, self.user.user.username) + return "Submission %d of %s by %s" % ( + self.id, + self.problem, + self.user.user.username, + ) def get_absolute_url(self): - return reverse('submission_status', args=(self.id,)) + return reverse("submission_status", args=(self.id,)) @cached_property def contest_or_none(self): @@ -164,8 +209,14 @@ class Submission(models.Model): @classmethod def get_id_secret(cls, sub_id): - return (hmac.new(utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), b'%d' % sub_id, hashlib.sha512) - .hexdigest()[:16] + '%08x' % sub_id) + return ( + hmac.new( + utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), + b"%d" % sub_id, + hashlib.sha512, + ).hexdigest()[:16] + + "%08x" % sub_id + ) @cached_property def id_secret(self): @@ -173,47 +224,61 @@ class Submission(models.Model): class Meta: permissions = ( - ('abort_any_submission', 'Abort any submission'), - ('rejudge_submission', 'Rejudge the submission'), - ('rejudge_submission_lot', 'Rejudge a lot of submissions'), - ('spam_submission', 'Submit without limit'), - ('view_all_submission', 'View all submission'), - ('resubmit_other', "Resubmit others' submission"), + ("abort_any_submission", "Abort any submission"), + ("rejudge_submission", "Rejudge the submission"), + ("rejudge_submission_lot", "Rejudge a lot of submissions"), + ("spam_submission", "Submit without limit"), + ("view_all_submission", "View all submission"), + ("resubmit_other", "Resubmit others' submission"), ) - verbose_name = _('submission') - verbose_name_plural = _('submissions') + verbose_name = _("submission") + verbose_name_plural = _("submissions") class SubmissionSource(models.Model): - submission = models.OneToOneField(Submission, on_delete=models.CASCADE, verbose_name=_('associated submission'), - related_name='source') - source = models.TextField(verbose_name=_('source code'), max_length=65536) + submission = models.OneToOneField( + Submission, + on_delete=models.CASCADE, + verbose_name=_("associated submission"), + related_name="source", + ) + source = models.TextField(verbose_name=_("source code"), max_length=65536) def __str__(self): - return 'Source of %s' % self.submission + return "Source of %s" % self.submission class SubmissionTestCase(models.Model): RESULT = SUBMISSION_RESULT - submission = models.ForeignKey(Submission, verbose_name=_('associated submission'), - related_name='test_cases', on_delete=models.CASCADE) - case = models.IntegerField(verbose_name=_('test case ID')) - status = models.CharField(max_length=3, verbose_name=_('status flag'), choices=SUBMISSION_RESULT) - time = models.FloatField(verbose_name=_('execution time'), null=True) - memory = models.FloatField(verbose_name=_('memory usage'), null=True) - points = models.FloatField(verbose_name=_('points granted'), null=True) - total = models.FloatField(verbose_name=_('points possible'), null=True) - batch = models.IntegerField(verbose_name=_('batch number'), null=True) - feedback = models.CharField(max_length=50, verbose_name=_('judging feedback'), blank=True) - extended_feedback = models.TextField(verbose_name=_('extended judging feedback'), blank=True) - output = models.TextField(verbose_name=_('program output'), blank=True) + submission = models.ForeignKey( + Submission, + verbose_name=_("associated submission"), + related_name="test_cases", + on_delete=models.CASCADE, + ) + case = models.IntegerField(verbose_name=_("test case ID")) + status = models.CharField( + max_length=3, verbose_name=_("status flag"), choices=SUBMISSION_RESULT + ) + time = models.FloatField(verbose_name=_("execution time"), null=True) + memory = models.FloatField(verbose_name=_("memory usage"), null=True) + points = models.FloatField(verbose_name=_("points granted"), null=True) + total = models.FloatField(verbose_name=_("points possible"), null=True) + batch = models.IntegerField(verbose_name=_("batch number"), null=True) + feedback = models.CharField( + max_length=50, verbose_name=_("judging feedback"), blank=True + ) + extended_feedback = models.TextField( + verbose_name=_("extended judging feedback"), blank=True + ) + output = models.TextField(verbose_name=_("program output"), blank=True) @property def long_status(self): - return Submission.USER_DISPLAY_CODES.get(self.status, '') + return Submission.USER_DISPLAY_CODES.get(self.status, "") class Meta: - unique_together = ('submission', 'case') - verbose_name = _('submission test case') - verbose_name_plural = _('submission test cases') + unique_together = ("submission", "case") + verbose_name = _("submission test case") + verbose_name_plural = _("submission test cases") diff --git a/judge/models/ticket.py b/judge/models/ticket.py index 9b3fc54..27d6fb4 100644 --- a/judge/models/ticket.py +++ b/judge/models/ticket.py @@ -7,24 +7,43 @@ from judge.models.profile import Profile class Ticket(models.Model): - title = models.CharField(max_length=100, verbose_name=_('ticket title')) - user = models.ForeignKey(Profile, verbose_name=_('ticket creator'), related_name='tickets', - on_delete=models.CASCADE) - time = models.DateTimeField(verbose_name=_('creation time'), auto_now_add=True) - assignees = models.ManyToManyField(Profile, verbose_name=_('assignees'), related_name='assigned_tickets') - notes = models.TextField(verbose_name=_('quick notes'), blank=True, - help_text=_('Staff notes for this issue to aid in processing.')) - content_type = models.ForeignKey(ContentType, verbose_name=_('linked item type'), - on_delete=models.CASCADE) - object_id = models.PositiveIntegerField(verbose_name=_('linked item ID')) + title = models.CharField(max_length=100, verbose_name=_("ticket title")) + user = models.ForeignKey( + Profile, + verbose_name=_("ticket creator"), + related_name="tickets", + on_delete=models.CASCADE, + ) + time = models.DateTimeField(verbose_name=_("creation time"), auto_now_add=True) + assignees = models.ManyToManyField( + Profile, verbose_name=_("assignees"), related_name="assigned_tickets" + ) + notes = models.TextField( + verbose_name=_("quick notes"), + blank=True, + help_text=_("Staff notes for this issue to aid in processing."), + ) + content_type = models.ForeignKey( + ContentType, verbose_name=_("linked item type"), on_delete=models.CASCADE + ) + object_id = models.PositiveIntegerField(verbose_name=_("linked item ID")) linked_item = GenericForeignKey() - is_open = models.BooleanField(verbose_name=_('is ticket open?'), default=True) + is_open = models.BooleanField(verbose_name=_("is ticket open?"), default=True) class TicketMessage(models.Model): - ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'), related_name='messages', - related_query_name='message', on_delete=models.CASCADE) - user = models.ForeignKey(Profile, verbose_name=_('poster'), related_name='ticket_messages', - on_delete=models.CASCADE) - body = models.TextField(verbose_name=_('message body')) - time = models.DateTimeField(verbose_name=_('message time'), auto_now_add=True) + ticket = models.ForeignKey( + Ticket, + verbose_name=_("ticket"), + related_name="messages", + related_query_name="message", + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + Profile, + verbose_name=_("poster"), + related_name="ticket_messages", + on_delete=models.CASCADE, + ) + body = models.TextField(verbose_name=_("message body")) + time = models.DateTimeField(verbose_name=_("message time"), auto_now_add=True) diff --git a/judge/models/volunteer.py b/judge/models/volunteer.py index 90c351c..5d3babd 100644 --- a/judge/models/volunteer.py +++ b/judge/models/volunteer.py @@ -4,25 +4,36 @@ from django.utils.translation import gettext_lazy as _ from judge.models import Profile, Problem, ProblemType -__all__ = ['VolunteerProblemVote'] +__all__ = ["VolunteerProblemVote"] + class VolunteerProblemVote(models.Model): - voter = models.ForeignKey(Profile, related_name='volunteer_problem_votes', on_delete=CASCADE) - problem = models.ForeignKey(Problem, related_name='volunteer_user_votes', on_delete=CASCADE) + voter = models.ForeignKey( + Profile, related_name="volunteer_problem_votes", on_delete=CASCADE + ) + problem = models.ForeignKey( + Problem, related_name="volunteer_user_votes", on_delete=CASCADE + ) time = models.DateTimeField(auto_now_add=True) - knowledge_points = models.PositiveIntegerField(verbose_name=_('knowledge points'), - help_text=_('Points awarded by knowledge difficulty')) - thinking_points = models.PositiveIntegerField(verbose_name=_('thinking points'), - help_text=_('Points awarded by thinking difficulty')) - types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'), - help_text=_('The type of problem, ' - "as shown on the problem's page.")) - feedback = models.TextField(verbose_name=_('feedback'), blank=True) + knowledge_points = models.PositiveIntegerField( + verbose_name=_("knowledge points"), + help_text=_("Points awarded by knowledge difficulty"), + ) + thinking_points = models.PositiveIntegerField( + verbose_name=_("thinking points"), + help_text=_("Points awarded by thinking difficulty"), + ) + types = models.ManyToManyField( + ProblemType, + verbose_name=_("problem types"), + help_text=_("The type of problem, " "as shown on the problem's page."), + ) + feedback = models.TextField(verbose_name=_("feedback"), blank=True) class Meta: - verbose_name = _('volunteer vote') - verbose_name_plural = _('volunteer votes') - unique_together = ['voter', 'problem'] + verbose_name = _("volunteer vote") + verbose_name_plural = _("volunteer votes") + unique_together = ["voter", "problem"] def __str__(self): - return f'{self.voter} for {self.problem.code}' + return f"{self.voter} for {self.problem.code}" diff --git a/judge/pdf_problems.py b/judge/pdf_problems.py index a5b318f..4cd4a14 100644 --- a/judge/pdf_problems.py +++ b/judge/pdf_problems.py @@ -11,7 +11,7 @@ import uuid from django.conf import settings from django.utils.translation import gettext -logger = logging.getLogger('judge.problem.pdf') +logger = logging.getLogger("judge.problem.pdf") HAS_SELENIUM = False if settings.USE_SELENIUM: @@ -21,9 +21,10 @@ if settings.USE_SELENIUM: 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) + 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) @@ -32,27 +33,30 @@ NODE_PATH = settings.NODEJS 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 or HAS_SELENIUM)) +HAS_PDF = os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and ( + HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER or HAS_SELENIUM +) EXIFTOOL = settings.EXIFTOOL HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK) class BasePdfMaker(object): - math_engine = 'jax' + math_engine = "jax" title = None def __init__(self, dir=None, clean_up=True): - self.dir = dir or os.path.join(settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1())) + self.dir = dir or os.path.join( + settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1()) + ) self.proc = None self.log = None - self.htmlfile = os.path.join(self.dir, 'input.html') - self.pdffile = os.path.join(self.dir, 'output.pdf') + self.htmlfile = os.path.join(self.dir, "input.html") + self.pdffile = os.path.join(self.dir, "output.pdf") self.clean_up = clean_up def load(self, file, source): - with open(os.path.join(self.dir, file), 'w') as target, open(source) as source: + with open(os.path.join(self.dir, file), "w") as target, open(source) as source: target.write(source.read()) def make(self, debug=False): @@ -60,21 +64,27 @@ class BasePdfMaker(object): if self.title and HAS_EXIFTOOL: try: - subprocess.check_output([EXIFTOOL, '-Title=%s' % (self.title,), self.pdffile]) + subprocess.check_output( + [EXIFTOOL, "-Title=%s" % (self.title,), self.pdffile] + ) except subprocess.CalledProcessError as e: - logger.error('Failed to run exiftool to set title for: %s\n%s', self.title, e.output) + logger.error( + "Failed to run exiftool to set title for: %s\n%s", + self.title, + e.output, + ) def _make(self, debug): raise NotImplementedError() @property def html(self): - with io.open(self.htmlfile, encoding='utf-8') as f: + with io.open(self.htmlfile, encoding="utf-8") as f: return f.read() @html.setter def html(self, data): - with io.open(self.htmlfile, 'w', encoding='utf-8') as f: + with io.open(self.htmlfile, "w", encoding="utf-8") as f: f.write(data) @property @@ -99,7 +109,7 @@ class BasePdfMaker(object): class PhantomJSPdfMaker(BasePdfMaker): - template = '''\ + template = """\ "use strict"; var page = require('webpage').create(); var param = {params}; @@ -136,29 +146,37 @@ page.open(param.input, function (status) { }, param.timeout); } }); -''' +""" def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'zoom': settings.PHANTOMJS_PDF_ZOOM, - 'timeout': int(settings.PHANTOMJS_PDF_TIMEOUT * 1000), - 'input': 'input.html', 'output': 'output.pdf', - 'paper': settings.PHANTOMJS_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]'), - })) + return self.template.replace( + "{params}", + json.dumps( + { + "zoom": settings.PHANTOMJS_PDF_ZOOM, + "timeout": int(settings.PHANTOMJS_PDF_TIMEOUT * 1000), + "input": "input.html", + "output": "output.pdf", + "paper": settings.PHANTOMJS_PAPER_SIZE, + "footer": gettext("Page [page] of [topage]"), + } + ), + ) def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: + with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) - cmdline = [settings.PHANTOMJS, '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir) + cmdline = [settings.PHANTOMJS, "_render.js"] + self.proc = subprocess.Popen( + cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir + ) self.log = self.proc.communicate()[0] class SlimerJSPdfMaker(BasePdfMaker): - math_engine = 'mml' + math_engine = "mml" - template = '''\ + template = """\ "use strict"; try { var param = {params}; @@ -189,33 +207,47 @@ try { console.error(e); slimer.exit(1); } -''' +""" def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'zoom': settings.SLIMERJS_PDF_ZOOM, - 'input': 'input.html', 'output': 'output.pdf', - 'paper': settings.SLIMERJS_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]').replace('[page]', '&P').replace('[topage]', '&L'), - })) + return self.template.replace( + "{params}", + json.dumps( + { + "zoom": settings.SLIMERJS_PDF_ZOOM, + "input": "input.html", + "output": "output.pdf", + "paper": settings.SLIMERJS_PAPER_SIZE, + "footer": gettext("Page [page] of [topage]") + .replace("[page]", "&P") + .replace("[topage]", "&L"), + } + ), + ) def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: + with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) env = None firefox = settings.SLIMERJS_FIREFOX_PATH if firefox: env = os.environ.copy() - env['SLIMERJSLAUNCHER'] = firefox + env["SLIMERJSLAUNCHER"] = firefox - cmdline = [settings.SLIMERJS, '--headless', '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env) + cmdline = [settings.SLIMERJS, "--headless", "_render.js"] + self.proc = subprocess.Popen( + cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.dir, + env=env, + ) self.log = self.proc.communicate()[0] class PuppeteerPDFRender(BasePdfMaker): - template = '''\ + template = """\ "use strict"; const param = {params}; const puppeteer = require('puppeteer'); @@ -249,67 +281,81 @@ puppeteer.launch().then(browser => Promise.resolve() console.error(e); process.exit(1); }); -''' +""" def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'input': 'file://%s' % self.htmlfile, - 'output': self.pdffile, - 'paper': settings.PUPPETEER_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]'), - })) + return self.template.replace( + "{params}", + json.dumps( + { + "input": "file://%s" % self.htmlfile, + "output": self.pdffile, + "paper": settings.PUPPETEER_PAPER_SIZE, + "footer": gettext("Page [page] of [topage]"), + } + ), + ) def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: + with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) env = os.environ.copy() - env['NODE_PATH'] = os.path.dirname(PUPPETEER_MODULE) + env["NODE_PATH"] = os.path.dirname(PUPPETEER_MODULE) - cmdline = [NODE_PATH, '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env) + cmdline = [NODE_PATH, "_render.js"] + 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': '
', - 'footerTemplate': '
' + - gettext('Page %s of %s') % - ('', '') + - '
', + "printBackground": True, + "displayHeaderFooter": True, + "headerTemplate": "
", + "footerTemplate": '
' + + gettext("Page %s of %s") + % ('', '') + + "
", } def get_log(self, driver): - return '\n'.join(map(str, driver.get_log('driver') + driver.get_log('browser'))) + 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.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) + 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'))) + 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' + 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) + 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'])) + with open(self.pdffile, "wb") as f: + f.write(base64.b64decode(response["data"])) self.success = True browser.quit() diff --git a/judge/performance_points.py b/judge/performance_points.py index eee3c10..63a7636 100644 --- a/judge/performance_points.py +++ b/judge/performance_points.py @@ -6,16 +6,22 @@ from django.db import connection from judge.models import Submission from judge.timezone import from_database_time -PP_WEIGHT_TABLE = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] +PP_WEIGHT_TABLE = [ + pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES) +] -PPBreakdown = namedtuple('PPBreakdown', 'points weight scaled_points problem_name problem_code ' - 'sub_id sub_date sub_points sub_total sub_result_class ' - 'sub_short_status sub_long_status sub_lang') +PPBreakdown = namedtuple( + "PPBreakdown", + "points weight scaled_points problem_name problem_code " + "sub_id sub_date sub_points sub_total sub_result_class " + "sub_short_status sub_long_status sub_lang", +) def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): with connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT max_points_table.problem_code, max_points_table.problem_name, max_points_table.max_points, @@ -47,32 +53,49 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): GROUP BY max_points_table.problem_id ORDER BY max_points DESC, judge_submission.date DESC LIMIT %s OFFSET %s - ''', (user.id, user.id, end - start + 1, start)) + """, + (user.id, user.id, end - start + 1, start), + ) data = cursor.fetchall() breakdown = [] for weight, contrib in zip(PP_WEIGHT_TABLE[start:end], data): - code, name, points, id, date, case_points, case_total, result, lang_short_name, lang_key = contrib + ( + code, + name, + points, + id, + date, + case_points, + case_total, + result, + lang_short_name, + lang_key, + ) = contrib # Replicates a lot of the logic usually done on Submission objects lang_short_display_name = lang_short_name or lang_key - result_class = Submission.result_class_from_code(result, case_points, case_total) - long_status = Submission.USER_DISPLAY_CODES.get(result, '') + result_class = Submission.result_class_from_code( + result, case_points, case_total + ) + long_status = Submission.USER_DISPLAY_CODES.get(result, "") - breakdown.append(PPBreakdown( - points=points, - weight=weight * 100, - scaled_points=points * weight, - problem_name=name, - problem_code=code, - sub_id=id, - sub_date=from_database_time(date), - sub_points=case_points, - sub_total=case_total, - sub_short_status=result, - sub_long_status=long_status, - sub_result_class=result_class, - sub_lang=lang_short_display_name, - )) + breakdown.append( + PPBreakdown( + points=points, + weight=weight * 100, + scaled_points=points * weight, + problem_name=name, + problem_code=code, + sub_id=id, + sub_date=from_database_time(date), + sub_points=case_points, + sub_total=case_total, + sub_short_status=result, + sub_long_status=long_status, + sub_result_class=result_class, + sub_lang=lang_short_display_name, + ) + ) has_more = end < min(len(PP_WEIGHT_TABLE), start + len(data)) return breakdown, has_more diff --git a/judge/ratings.py b/judge/ratings.py index 9d0f36d..de2c313 100644 --- a/judge/ratings.py +++ b/judge/ratings.py @@ -8,19 +8,21 @@ from django.db.models.functions import Coalesce from django.utils import timezone -BETA2 = 328.33 ** 2 -RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling -MEAN_INIT = 1400. +BETA2 = 328.33**2 +RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling +MEAN_INIT = 1400.0 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 +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 tie_ranker(iterable, key=attrgetter('points')): +def tie_ranker(iterable, key=attrgetter("points")): rank = 0 delta = 1 last = None @@ -71,15 +73,15 @@ def solve(tanh_terms, y_tg, lin_factor=0, bounds=VALID_RANGE): def get_var(times_ranked, cache=[VAR_INIT]): while times_ranked >= len(cache): - next_var = 1. / (1. / (cache[-1] + VAR_PER_CONTEST) + 1. / BETA2) + next_var = 1.0 / (1.0 / (cache[-1] + VAR_PER_CONTEST) + 1.0 / 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 + new_p = [0.0] * n + new_mean = [0.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] @@ -90,10 +92,10 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): 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 + if s > r: # s loses to r + y_tg += 1.0 / d + elif s < r: # s beats r + y_tg -= 1.0 / 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) @@ -117,10 +119,10 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): # Calculate mean. for i, r in enumerate(ranking): tanh_terms = [] - w_prev = 1. - w_sum = 0. + w_prev = 1.0 + w_sum = 0.0 for j, h in enumerate([new_p[i]] + historical_p[i]): - gamma2 = (VAR_PER_CONTEST if j > 0 else 0) + 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 @@ -128,13 +130,16 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): 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 + w0 = 1.0 / 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)] + 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 @@ -142,17 +147,39 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p): def rate_contest(contest): from judge.models import 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]), 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') + 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: @@ -161,38 +188,76 @@ def rate_contest(contest): 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)) + 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']) + 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) + rating, mean, performance = recalculate_ratings( + ranking, old_mean, times_ranked, historical_p + ) now = timezone.now() - 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)] + 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.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])) + 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_LEVELS = [ + "Newbie", + "Amateur", + "Expert", + "Candidate Master", + "Master", + "Grandmaster", + "Target", +] 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'] +RATING_CLASS = [ + "rate-newbie", + "rate-amateur", + "rate-specialist", + "rate-expert", + "rate-candidate-master", + "rate-master", + "rate-grandmaster", + "rate-target", +] def rating_level(rating): @@ -213,4 +278,4 @@ def rating_progress(rating): return 1.0 prev = 0 if not level else RATING_VALUES[level - 1] next = RATING_VALUES[level] - return (rating - prev + 0.0) / (next - prev) \ No newline at end of file + return (rating - prev + 0.0) / (next - prev) diff --git a/judge/signals.py b/judge/signals.py index e03c17c..e308f98 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -9,8 +9,21 @@ from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from .caching import finished_submission -from .models import BlogPost, Comment, Contest, ContestSubmission, EFFECTIVE_MATH_ENGINES, Judge, Language, License, \ - MiscConfig, Organization, Problem, Profile, Submission +from .models import ( + BlogPost, + Comment, + Contest, + ContestSubmission, + EFFECTIVE_MATH_ENGINES, + Judge, + Language, + License, + MiscConfig, + Organization, + Problem, + Profile, + Submission, +) def get_pdf_path(basename): @@ -27,75 +40,109 @@ def unlink_if_exists(file): @receiver(post_save, sender=Problem) def problem_update(sender, instance, **kwargs): - if hasattr(instance, '_updating_stats_only'): + if hasattr(instance, "_updating_stats_only"): return - cache.delete_many([ - make_template_fragment_key('submission_problem', (instance.id,)), - make_template_fragment_key('problem_feed', (instance.id,)), - 'problem_tls:%s' % instance.id, 'problem_mls:%s' % instance.id, - ]) - cache.delete_many([make_template_fragment_key('problem_html', (instance.id, engine, lang)) - for lang, _ in settings.LANGUAGES for engine in EFFECTIVE_MATH_ENGINES]) - cache.delete_many([make_template_fragment_key('problem_authors', (instance.id, lang)) - for lang, _ in settings.LANGUAGES]) - cache.delete_many(['generated-meta-problem:%s:%d' % (lang, instance.id) for lang, _ in settings.LANGUAGES]) + cache.delete_many( + [ + make_template_fragment_key("submission_problem", (instance.id,)), + make_template_fragment_key("problem_feed", (instance.id,)), + "problem_tls:%s" % instance.id, + "problem_mls:%s" % instance.id, + ] + ) + cache.delete_many( + [ + make_template_fragment_key("problem_html", (instance.id, engine, lang)) + for lang, _ in settings.LANGUAGES + for engine in EFFECTIVE_MATH_ENGINES + ] + ) + cache.delete_many( + [ + make_template_fragment_key("problem_authors", (instance.id, lang)) + for lang, _ in settings.LANGUAGES + ] + ) + cache.delete_many( + [ + "generated-meta-problem:%s:%d" % (lang, instance.id) + for lang, _ in settings.LANGUAGES + ] + ) for lang, _ in settings.LANGUAGES: - unlink_if_exists(get_pdf_path('%s.%s.pdf' % (instance.code, lang))) + unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang))) @receiver(post_save, sender=Profile) def profile_update(sender, instance, **kwargs): - if hasattr(instance, '_updating_stats_only'): + if hasattr(instance, "_updating_stats_only"): return - cache.delete_many([make_template_fragment_key('user_about', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES] + - [make_template_fragment_key('org_member_count', (org_id,)) - for org_id in instance.organizations.values_list('id', flat=True)]) + cache.delete_many( + [ + make_template_fragment_key("user_about", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + + [ + make_template_fragment_key("org_member_count", (org_id,)) + for org_id in instance.organizations.values_list("id", flat=True) + ] + ) @receiver(post_save, sender=Contest) def contest_update(sender, instance, **kwargs): - if hasattr(instance, '_updating_stats_only'): + if hasattr(instance, "_updating_stats_only"): return - cache.delete_many(['generated-meta-contest:%d' % instance.id] + - [make_template_fragment_key('contest_html', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES]) + cache.delete_many( + ["generated-meta-contest:%d" % instance.id] + + [ + make_template_fragment_key("contest_html", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + ) @receiver(post_save, sender=License) def license_update(sender, instance, **kwargs): - cache.delete(make_template_fragment_key('license_html', (instance.id,))) + cache.delete(make_template_fragment_key("license_html", (instance.id,))) @receiver(post_save, sender=Language) def language_update(sender, instance, **kwargs): - cache.delete_many([make_template_fragment_key('language_html', (instance.id,)), - 'lang:cn_map']) + cache.delete_many( + [make_template_fragment_key("language_html", (instance.id,)), "lang:cn_map"] + ) @receiver(post_save, sender=Judge) def judge_update(sender, instance, **kwargs): - cache.delete(make_template_fragment_key('judge_html', (instance.id,))) + cache.delete(make_template_fragment_key("judge_html", (instance.id,))) @receiver(post_save, sender=Comment) def comment_update(sender, instance, **kwargs): - cache.delete('comment_feed:%d' % instance.id) + cache.delete("comment_feed:%d" % instance.id) @receiver(post_save, sender=BlogPost) def post_update(sender, instance, **kwargs): - cache.delete_many([ - make_template_fragment_key('post_summary', (instance.id,)), - 'blog_slug:%d' % instance.id, - 'blog_feed:%d' % instance.id, - ]) - cache.delete_many([make_template_fragment_key('post_content', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES]) + cache.delete_many( + [ + make_template_fragment_key("post_summary", (instance.id,)), + "blog_slug:%d" % instance.id, + "blog_feed:%d" % instance.id, + ] + ) + cache.delete_many( + [ + make_template_fragment_key("post_content", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + ) @receiver(post_delete, sender=Submission) @@ -112,21 +159,31 @@ def contest_submission_delete(sender, instance, **kwargs): @receiver(post_save, sender=Organization) def organization_update(sender, instance, **kwargs): - cache.delete_many([make_template_fragment_key('organization_html', (instance.id, engine)) - for engine in EFFECTIVE_MATH_ENGINES]) + cache.delete_many( + [ + make_template_fragment_key("organization_html", (instance.id, engine)) + for engine in EFFECTIVE_MATH_ENGINES + ] + ) _misc_config_i18n = [code for code, _ in settings.LANGUAGES] -_misc_config_i18n.append('') +_misc_config_i18n.append("") @receiver(post_save, sender=MiscConfig) def misc_config_update(sender, instance, **kwargs): - cache.delete_many(['misc_config:%s:%s:%s' % (domain, lang, instance.key.split('.')[0]) - for lang in _misc_config_i18n - for domain in Site.objects.values_list('domain', flat=True)]) + cache.delete_many( + [ + "misc_config:%s:%s:%s" % (domain, lang, instance.key.split(".")[0]) + for lang in _misc_config_i18n + for domain in Site.objects.values_list("domain", flat=True) + ] + ) @receiver(post_save, sender=ContestSubmission) def contest_submission_update(sender, instance, **kwargs): - Submission.objects.filter(id=instance.submission_id).update(contest_object_id=instance.participation.contest_id) + Submission.objects.filter(id=instance.submission_id).update( + contest_object_id=instance.participation.contest_id + ) diff --git a/judge/sitemap.py b/judge/sitemap.py index 2deea97..dd71525 100644 --- a/judge/sitemap.py +++ b/judge/sitemap.py @@ -7,80 +7,83 @@ from judge.models import BlogPost, Contest, Organization, Problem, Solution class ProblemSitemap(Sitemap): - changefreq = 'daily' + changefreq = "daily" priority = 0.8 def items(self): - return Problem.get_public_problems().values_list('code') + return Problem.get_public_problems().values_list("code") def location(self, obj): - return reverse('problem_detail', args=obj) + return reverse("problem_detail", args=obj) class UserSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.5 def items(self): - return User.objects.values_list('username') + return User.objects.values_list("username") def location(self, obj): - return reverse('user_page', args=obj) + return reverse("user_page", args=obj) class ContestSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.5 def items(self): - return Contest.objects.filter(is_visible=True, is_private=False, - is_organization_private=False).values_list('key') + return Contest.objects.filter( + is_visible=True, is_private=False, is_organization_private=False + ).values_list("key") def location(self, obj): - return reverse('contest_view', args=obj) + return reverse("contest_view", args=obj) class OrganizationSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.5 def items(self): - return Organization.objects.values_list('id', 'slug') + return Organization.objects.values_list("id", "slug") def location(self, obj): - return reverse('organization_home', args=obj) + return reverse("organization_home", args=obj) class BlogPostSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.7 def items(self): - return (BlogPost.objects.filter(visible=True, is_organization_private=False, publish_on__lte=timezone.now()) - .values_list('id', 'slug')) - + return BlogPost.objects.filter( + visible=True, is_organization_private=False, publish_on__lte=timezone.now() + ).values_list("id", "slug") + def location(self, obj): - return reverse('blog_post', args=obj) + return reverse("blog_post", args=obj) class SolutionSitemap(Sitemap): - changefreq = 'hourly' + changefreq = "hourly" priority = 0.8 def items(self): - return (Solution.objects.filter(is_public=True, publish_on__lte=timezone.now(), problem__isnull=False) - .values_list('problem__code')) + return Solution.objects.filter( + is_public=True, publish_on__lte=timezone.now(), problem__isnull=False + ).values_list("problem__code") def location(self, obj): - return reverse('problem_editorial', args=obj) + return reverse("problem_editorial", args=obj) class HomePageSitemap(Sitemap): priority = 1.0 - changefreq = 'daily' + changefreq = "daily" def items(self): - return ['home'] + return ["home"] def location(self, obj): return reverse(obj) @@ -94,10 +97,10 @@ class UrlSitemap(Sitemap): return self.pages def location(self, obj): - return obj['location'] if isinstance(obj, dict) else obj + return obj["location"] if isinstance(obj, dict) else obj def priority(self, obj): - return obj.get('priority', 0.5) if isinstance(obj, dict) else 0.5 + return obj.get("priority", 0.5) if isinstance(obj, dict) else 0.5 def changefreq(self, obj): - return obj.get('changefreq', 'daily') if isinstance(obj, dict) else 'daily' + return obj.get("changefreq", "daily") if isinstance(obj, dict) else "daily" diff --git a/judge/social_auth.py b/judge/social_auth.py index 7546010..71a12bb 100644 --- a/judge/social_auth.py +++ b/judge/social_auth.py @@ -14,53 +14,65 @@ from reversion import revisions from social_core.backends.github import GithubOAuth2 from social_core.exceptions import InvalidEmail, SocialAuthBaseException from social_core.pipeline.partial import partial -from social_django.middleware import SocialAuthExceptionMiddleware as OldSocialAuthExceptionMiddleware +from social_django.middleware import ( + SocialAuthExceptionMiddleware as OldSocialAuthExceptionMiddleware, +) from judge.forms import ProfileForm from judge.models import Language, Profile -logger = logging.getLogger('judge.social_auth') +logger = logging.getLogger("judge.social_auth") class GitHubSecureEmailOAuth2(GithubOAuth2): - name = 'github-secure' + name = "github-secure" def user_data(self, access_token, *args, **kwargs): data = self._user_data(access_token) try: - emails = self._user_data(access_token, '/emails') + emails = self._user_data(access_token, "/emails") except (HTTPError, ValueError, TypeError): emails = [] - emails = [(e.get('email'), e.get('primary'), 0) for e in emails if isinstance(e, dict) and e.get('verified')] + emails = [ + (e.get("email"), e.get("primary"), 0) + for e in emails + if isinstance(e, dict) and e.get("verified") + ] emails.sort(key=itemgetter(1), reverse=True) emails = list(map(itemgetter(0), emails)) if emails: - data['email'] = emails[0] + data["email"] = emails[0] else: - data['email'] = None + data["email"] = None return data -def slugify_username(username, renotword=re.compile(r'[^\w]')): - return renotword.sub('', username.replace('-', '_')) +def slugify_username(username, renotword=re.compile(r"[^\w]")): + return renotword.sub("", username.replace("-", "_")) def verify_email(backend, details, *args, **kwargs): - if not details['email']: + if not details["email"]: raise InvalidEmail(backend) class UsernameForm(forms.Form): - username = forms.RegexField(regex=r'^\w+$', max_length=30, label='Username', - error_messages={'invalid': 'A username must contain letters, numbers, or underscores'}) + username = forms.RegexField( + regex=r"^\w+$", + max_length=30, + label="Username", + error_messages={ + "invalid": "A username must contain letters, numbers, or underscores" + }, + ) def clean_username(self): - if User.objects.filter(username=self.cleaned_data['username']).exists(): - raise forms.ValidationError('Sorry, the username is taken.') - return self.cleaned_data['username'] + if User.objects.filter(username=self.cleaned_data["username"]).exists(): + raise forms.ValidationError("Sorry, the username is taken.") + return self.cleaned_data["username"] @partial @@ -70,21 +82,26 @@ def choose_username(backend, user, username=None, *args, **kwargs): if request.POST: form = UsernameForm(request.POST) if form.is_valid(): - return {'username': form.cleaned_data['username']} + return {"username": form.cleaned_data["username"]} else: - form = UsernameForm(initial={'username': username}) - return render(request, 'registration/username_select.html', { - 'title': 'Choose a username', 'form': form, - }) + form = UsernameForm(initial={"username": username}) + return render( + request, + "registration/username_select.html", + { + "title": "Choose a username", + "form": form, + }, + ) @partial def make_profile(backend, user, response, is_new=False, *args, **kwargs): if is_new: - if not hasattr(user, 'profile'): + if not hasattr(user, "profile"): profile = Profile(user=user) profile.language = Language.get_default_language() - logger.info('Info from %s: %s', backend.name, response) + logger.info("Info from %s: %s", backend.name, response) profile.save() form = ProfileForm(instance=profile, user=user) else: @@ -95,15 +112,25 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs): with transaction.atomic(), revisions.create_revision(): form.save() revisions.set_user(user) - revisions.set_comment('Updated on registration') + revisions.set_comment("Updated on registration") return - return render(backend.strategy.request, 'registration/profile_creation.html', { - 'title': 'Create your profile', 'form': form, - }) + return render( + backend.strategy.request, + "registration/profile_creation.html", + { + "title": "Create your profile", + "form": form, + }, + ) class SocialAuthExceptionMiddleware(OldSocialAuthExceptionMiddleware): def process_exception(self, request, exception): if isinstance(exception, SocialAuthBaseException): - return HttpResponseRedirect('%s?message=%s' % (reverse('social_auth_error'), - quote(self.get_message(request, exception)))) + return HttpResponseRedirect( + "%s?message=%s" + % ( + reverse("social_auth_error"), + quote(self.get_message(request, exception)), + ) + ) diff --git a/judge/tasks/contest.py b/judge/tasks/contest.py index 0c36e23..ca896a9 100644 --- a/judge/tasks/contest.py +++ b/judge/tasks/contest.py @@ -7,7 +7,7 @@ from moss import MOSS from judge.models import Contest, ContestMoss, ContestParticipation, Submission from judge.utils.celery import Progress -__all__ = ('rescore_contest', 'run_moss') +__all__ = ("rescore_contest", "run_moss") @shared_task(bind=True) @@ -16,7 +16,9 @@ def rescore_contest(self, contest_key): participations = contest.users rescored = 0 - with Progress(self, participations.count(), stage=_('Recalculating contest scores')) as p: + with Progress( + self, participations.count(), stage=_("Recalculating contest scores") + ) as p: for participation in participations.iterator(): participation.recompute_results() rescored += 1 @@ -29,7 +31,7 @@ def rescore_contest(self, contest_key): def run_moss(self, contest_key): moss_api_key = settings.MOSS_API_KEY if moss_api_key is None: - raise ImproperlyConfigured('No MOSS API Key supplied') + raise ImproperlyConfigured("No MOSS API Key supplied") contest = Contest.objects.get(key=contest_key) ContestMoss.objects.filter(contest=contest).delete() @@ -37,21 +39,34 @@ def run_moss(self, contest_key): length = len(ContestMoss.LANG_MAPPING) * contest.problems.count() moss_results = [] - with Progress(self, length, stage=_('Running MOSS')) as p: + with Progress(self, length, stage=_("Running MOSS")) as p: for problem in contest.problems.all(): for dmoj_lang, moss_lang in ContestMoss.LANG_MAPPING: - result = ContestMoss(contest=contest, problem=problem, language=dmoj_lang) + result = ContestMoss( + contest=contest, problem=problem, language=dmoj_lang + ) - subs = Submission.objects.filter( - contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE), - contest_object=contest, - problem=problem, - language__common_name=dmoj_lang, - ).order_by('-points').values_list('user__user__username', 'source__source') + subs = ( + Submission.objects.filter( + contest__participation__virtual__in=( + ContestParticipation.LIVE, + ContestParticipation.SPECTATE, + ), + contest_object=contest, + problem=problem, + language__common_name=dmoj_lang, + ) + .order_by("-points") + .values_list("user__user__username", "source__source") + ) if subs.exists(): - moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100, - comment='%s - %s' % (contest.key, problem.code)) + moss_call = MOSS( + moss_api_key, + language=moss_lang, + matching_file_limit=100, + comment="%s - %s" % (contest.key, problem.code), + ) users = set() @@ -59,7 +74,7 @@ def run_moss(self, contest_key): if username in users: continue users.add(username) - moss_call.add_file_from_memory(username, source.encode('utf-8')) + moss_call.add_file_from_memory(username, source.encode("utf-8")) result.url = moss_call.process() result.submission_count = len(users) diff --git a/judge/tasks/demo.py b/judge/tasks/demo.py index c09dcbf..bd97401 100644 --- a/judge/tasks/demo.py +++ b/judge/tasks/demo.py @@ -4,7 +4,7 @@ from celery import shared_task from judge.utils.celery import Progress -__all__ = ('success', 'failure', 'progress') +__all__ = ("success", "failure", "progress") @shared_task @@ -14,7 +14,7 @@ def success(): @shared_task def failure(): - raise RuntimeError('This task always fails.') + raise RuntimeError("This task always fails.") @shared_task(bind=True) diff --git a/judge/tasks/experiment.py b/judge/tasks/experiment.py index 0e991c4..978c643 100644 --- a/judge/tasks/experiment.py +++ b/judge/tasks/experiment.py @@ -2,19 +2,20 @@ from judge.models import SubmissionTestCase, Problem from collections import defaultdict + def generate_report(problem): testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all() - + score = defaultdict(int) total = defaultdict(int) rate = defaultdict(int) for case in testcases.iterator(): - score[case.case] += int(case.status == 'AC') + score[case.case] += int(case.status == "AC") total[case.case] += 1 for i in score: rate[i] = score[i] / total[i] for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True): - print(i, score[i], total[i], rate[i]) \ No newline at end of file + print(i, score[i], total[i], rate[i]) diff --git a/judge/tasks/import_users.py b/judge/tasks/import_users.py index aeaeca5..db8d5c5 100644 --- a/judge/tasks/import_users.py +++ b/judge/tasks/import_users.py @@ -7,20 +7,23 @@ 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)'] +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')) + rows = csv.reader(csv_file.read().decode().split("\n")) header = next(rows) header = [i.lower() for i in header] - if 'username' not in header: + if "username" not in header: return [] res = [] @@ -28,55 +31,61 @@ def csv_to_dict(csv_file): for row in rows: if len(row) != len(header): continue - cur_dict = {i: '' for i in fields} + 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']: + if cur_dict["username"]: res.append(cur_dict) return res - + # return result log def import_users(users): - log = '' + log = "" for i, row in enumerate(users): - cur_log = str(i + 1) + '. ' + cur_log = str(i + 1) + ". " - username = row['username'] - cur_log += username + ': ' + username = row["username"] + cur_log += username + ": " - pwd = row['password'] - - user, created = User.objects.get_or_create(username=username, defaults={ - 'is_active': True, - }) + pwd = row["password"] - profile, _ = Profile.objects.get_or_create(user=user, defaults={ - 'language': Language.get_python3(), - 'timezone': settings.DEFAULT_USER_TIME_ZONE, - }) + 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 - ' + cur_log += "Create new - " else: - cur_log += 'Edit - ' + cur_log += "Edit - " if pwd: user.set_password(pwd) elif created: - user.set_password('lqdoj') - cur_log += 'Missing password, set password = lqdoj - ' + 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 "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 "school" in row.keys() and row["school"]: + user.last_name = row["school"] - if row['organizations']: - orgs = row['organizations'].split('&') + if row["organizations"]: + orgs = row["organizations"].split("&") added_orgs = [] for o in orgs: try: @@ -86,15 +95,15 @@ def import_users(users): except Organization.DoesNotExist: continue if added_orgs: - cur_log += 'Added to ' + ', '.join(added_orgs) + ' - ' + cur_log += "Added to " + ", ".join(added_orgs) + " - " + + if row["email"]: + user.email = row["email"] - if row['email']: - user.email = row['email'] - user.save() profile.save() - cur_log += 'Saved\n' + cur_log += "Saved\n" log += cur_log - log += 'FINISH' + log += "FINISH" - return log \ No newline at end of file + return log diff --git a/judge/tasks/submission.py b/judge/tasks/submission.py index a11de8c..5a5a8a9 100644 --- a/judge/tasks/submission.py +++ b/judge/tasks/submission.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext as _ from judge.models import Problem, Profile, Submission from judge.utils.celery import Progress -__all__ = ('apply_submission_filter', 'rejudge_problem_filter', 'rescore_problem') +__all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem") def apply_submission_filter(queryset, id_range, languages, results): @@ -21,7 +21,9 @@ def apply_submission_filter(queryset, id_range, languages, results): @shared_task(bind=True) -def rejudge_problem_filter(self, problem_id, id_range=None, languages=None, results=None): +def rejudge_problem_filter( + self, problem_id, id_range=None, languages=None, results=None +): queryset = Submission.objects.filter(problem_id=problem_id) queryset = apply_submission_filter(queryset, id_range, languages, results) @@ -40,27 +42,37 @@ def rescore_problem(self, problem_id): problem = Problem.objects.get(id=problem_id) submissions = Submission.objects.filter(problem_id=problem_id) - with Progress(self, submissions.count(), stage=_('Modifying submissions')) as p: + with Progress(self, submissions.count(), stage=_("Modifying submissions")) as p: rescored = 0 for submission in submissions.iterator(): - submission.points = round(submission.case_points / submission.case_total * problem.points - if submission.case_total else 0, 1) + submission.points = round( + submission.case_points / submission.case_total * problem.points + if submission.case_total + else 0, + 1, + ) if not problem.partial and submission.points < problem.points: submission.points = 0 - submission.save(update_fields=['points']) + submission.save(update_fields=["points"]) submission.update_contest() rescored += 1 if rescored % 10 == 0: p.done = rescored - with Progress(self, submissions.values('user_id').distinct().count(), stage=_('Recalculating user points')) as p: + with Progress( + self, + submissions.values("user_id").distinct().count(), + stage=_("Recalculating user points"), + ) as p: users = 0 - profiles = Profile.objects.filter(id__in=submissions.values_list('user_id', flat=True).distinct()) + profiles = Profile.objects.filter( + id__in=submissions.values_list("user_id", flat=True).distinct() + ) for profile in profiles.iterator(): profile._updating_stats_only = True profile.calculate_points() - cache.delete('user_complete:%d' % profile.id) - cache.delete('user_attempted:%d' % profile.id) + cache.delete("user_complete:%d" % profile.id) + cache.delete("user_attempted:%d" % profile.id) users += 1 if users % 10 == 0: p.done = users diff --git a/judge/template_context.py b/judge/template_context.py index 6348407..3c1900d 100644 --- a/judge/template_context.py +++ b/judge/template_context.py @@ -11,26 +11,26 @@ from .models import MiscConfig, NavigationBar, Profile class FixedSimpleLazyObject(SimpleLazyObject): - if not hasattr(SimpleLazyObject, '__iter__'): + if not hasattr(SimpleLazyObject, "__iter__"): __iter__ = new_method_proxy(iter) def get_resource(request): use_https = settings.DMOJ_SSL if use_https == 1: - scheme = 'https' if request.is_secure() else 'http' + scheme = "https" if request.is_secure() else "http" elif use_https > 1: - scheme = 'https' + scheme = "https" else: - scheme = 'http' + scheme = "http" return { - 'PYGMENT_THEME': settings.PYGMENT_THEME, - 'INLINE_JQUERY': settings.INLINE_JQUERY, - 'INLINE_FONTAWESOME': settings.INLINE_FONTAWESOME, - 'JQUERY_JS': settings.JQUERY_JS, - 'FONTAWESOME_CSS': settings.FONTAWESOME_CSS, - 'DMOJ_SCHEME': scheme, - 'DMOJ_CANONICAL': settings.DMOJ_CANONICAL, + "PYGMENT_THEME": settings.PYGMENT_THEME, + "INLINE_JQUERY": settings.INLINE_JQUERY, + "INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME, + "JQUERY_JS": settings.JQUERY_JS, + "FONTAWESOME_CSS": settings.FONTAWESOME_CSS, + "DMOJ_SCHEME": scheme, + "DMOJ_CANONICAL": settings.DMOJ_CANONICAL, } @@ -42,56 +42,63 @@ def get_profile(request): def comet_location(request): if request.is_secure(): - websocket = getattr(settings, 'EVENT_DAEMON_GET_SSL', settings.EVENT_DAEMON_GET) - poll = getattr(settings, 'EVENT_DAEMON_POLL_SSL', settings.EVENT_DAEMON_POLL) + websocket = getattr(settings, "EVENT_DAEMON_GET_SSL", settings.EVENT_DAEMON_GET) + poll = getattr(settings, "EVENT_DAEMON_POLL_SSL", settings.EVENT_DAEMON_POLL) else: websocket = settings.EVENT_DAEMON_GET poll = settings.EVENT_DAEMON_POLL - return {'EVENT_DAEMON_LOCATION': websocket, - 'EVENT_DAEMON_POLL_LOCATION': poll} + return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll} def __nav_tab(path): - result = list(NavigationBar.objects.extra(where=['%s REGEXP BINARY regex'], params=[path])[:1]) - return result[0].get_ancestors(include_self=True).values_list('key', flat=True) if result else [] + result = list( + NavigationBar.objects.extra(where=["%s REGEXP BINARY regex"], params=[path])[:1] + ) + return ( + result[0].get_ancestors(include_self=True).values_list("key", flat=True) + if result + else [] + ) def general_info(request): path = request.get_full_path() return { - 'nav_tab': FixedSimpleLazyObject(partial(__nav_tab, request.path)), - 'nav_bar': NavigationBar.objects.all(), - 'LOGIN_RETURN_PATH': '' if path.startswith('/accounts/') else path, - 'perms': PermWrapper(request.user), + "nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)), + "nav_bar": NavigationBar.objects.all(), + "LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path, + "perms": PermWrapper(request.user), } def site(request): - return {'site': get_current_site(request)} + return {"site": get_current_site(request)} class MiscConfigDict(dict): - __slots__ = ('language', 'site') + __slots__ = ("language", "site") - def __init__(self, language='', domain=None): + def __init__(self, language="", domain=None): self.language = language self.site = domain super(MiscConfigDict, self).__init__() def __missing__(self, key): - cache_key = 'misc_config:%s:%s:%s' % (self.site, self.language, key) + cache_key = "misc_config:%s:%s:%s" % (self.site, self.language, key) value = cache.get(cache_key) if value is None: - keys = ['%s.%s' % (key, self.language), key] if self.language else [key] + keys = ["%s.%s" % (key, self.language), key] if self.language else [key] if self.site is not None: - keys = ['%s:%s' % (self.site, key) for key in keys] + keys - map = dict(MiscConfig.objects.values_list('key', 'value').filter(key__in=keys)) + keys = ["%s:%s" % (self.site, key) for key in keys] + keys + map = dict( + MiscConfig.objects.values_list("key", "value").filter(key__in=keys) + ) for item in keys: if item in map: value = map[item] break else: - value = '' + value = "" cache.set(cache_key, value, 86400) self[key] = value return value @@ -99,23 +106,29 @@ class MiscConfigDict(dict): def misc_config(request): domain = get_current_site(request).domain - return {'misc_config': MiscConfigDict(domain=domain), - 'i18n_config': MiscConfigDict(language=request.LANGUAGE_CODE, domain=domain)} + return { + "misc_config": MiscConfigDict(domain=domain), + "i18n_config": MiscConfigDict(language=request.LANGUAGE_CODE, domain=domain), + } def site_name(request): - return {'SITE_NAME': settings.SITE_NAME, - 'SITE_LONG_NAME': settings.SITE_LONG_NAME, - 'SITE_ADMIN_EMAIL': settings.SITE_ADMIN_EMAIL} + return { + "SITE_NAME": settings.SITE_NAME, + "SITE_LONG_NAME": settings.SITE_LONG_NAME, + "SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL, + } def math_setting(request): - caniuse = CanIUse(request.META.get('HTTP_USER_AGENT', '')) + caniuse = CanIUse(request.META.get("HTTP_USER_AGENT", "")) if request.user.is_authenticated: engine = request.profile.math_engine else: engine = settings.MATHOID_DEFAULT_TYPE - if engine == 'auto': - engine = 'mml' if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else 'jax' - return {'MATH_ENGINE': engine, 'REQUIRE_JAX': engine == 'jax', 'caniuse': caniuse} + if engine == "auto": + engine = ( + "mml" if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else "jax" + ) + return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax", "caniuse": caniuse} diff --git a/judge/templatetags/dicts.py b/judge/templatetags/dicts.py index 3afc15b..efb321c 100644 --- a/judge/templatetags/dicts.py +++ b/judge/templatetags/dicts.py @@ -3,6 +3,6 @@ from django import template register = template.Library() -@register.filter(name='get_dict_item') +@register.filter(name="get_dict_item") def get_item(dictionary, key): return dictionary.get(key) diff --git a/judge/templatetags/list_processor.py b/judge/templatetags/list_processor.py index dc37105..15109cf 100644 --- a/judge/templatetags/list_processor.py +++ b/judge/templatetags/list_processor.py @@ -5,7 +5,7 @@ from django import template register = template.Library() -@register.filter(name='list_attr') +@register.filter(name="list_attr") def list_attr(iterable, prop): result = [] for item in iterable: @@ -15,43 +15,43 @@ def list_attr(iterable, prop): try: result.append(item[prop]) except KeyError: - result.append('') + result.append("") except TypeError: try: result.append(item[int(prop)]) except (IndexError, ValueError, TypeError): - result.append('') + result.append("") return result -@register.filter(name='list_getitem') +@register.filter(name="list_getitem") def list_getitem(iterable, prop): return list(map(itemgetter(prop), iterable)) -@register.filter(name='list_getindex') +@register.filter(name="list_getindex") def list_getindex(iterable, index): return list(map(itemgetter(int(index)), iterable)) -@register.filter(name='list_getattr') +@register.filter(name="list_getattr") def list_getattr(iterable, prop): return list(map(attrgetter(prop), iterable)) -@register.filter(name='sum_list') +@register.filter(name="sum_list") def sum_list(iterable): return sum(iterable) -@register.filter(name='max_list') +@register.filter(name="max_list") def max_list(iterable): if not iterable: return 0 return max(iterable) -@register.filter(name='min_list') +@register.filter(name="min_list") def min_list(iterable): if not iterable: return 0 diff --git a/judge/templatetags/strings.py b/judge/templatetags/strings.py index 6bd685a..475a615 100644 --- a/judge/templatetags/strings.py +++ b/judge/templatetags/strings.py @@ -3,16 +3,16 @@ from django import template register = template.Library() -@register.filter(name='split') +@register.filter(name="split") def split(value): - return value.split('\n') + return value.split("\n") -@register.filter(name='cutoff') +@register.filter(name="cutoff") def cutoff(value, length): - return value[:int(length)] + return value[: int(length)] -@register.filter(name='roundfloat') +@register.filter(name="roundfloat") def roundfloat(value, at): return str(round(value, int(at))) diff --git a/judge/user_log.py b/judge/user_log.py index d5304ff..91a4c67 100644 --- a/judge/user_log.py +++ b/judge/user_log.py @@ -10,12 +10,15 @@ class LogUserAccessMiddleware(object): def __call__(self, request): response = self.get_response(request) - if (hasattr(request, 'user') and request.user.is_authenticated and - not getattr(request, 'no_profile_update', False)): - updates = {'last_access': now()} + if ( + hasattr(request, "user") + and request.user.is_authenticated + and not getattr(request, "no_profile_update", False) + ): + updates = {"last_access": now()} # Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it. - if request.META.get('REMOTE_ADDR'): - updates['ip'] = request.META.get('REMOTE_ADDR') + if request.META.get("REMOTE_ADDR"): + updates["ip"] = request.META.get("REMOTE_ADDR") Profile.objects.filter(user_id=request.user.pk).update(**updates) return response diff --git a/judge/user_translations.py b/judge/user_translations.py index edc32ae..9932854 100644 --- a/judge/user_translations.py +++ b/judge/user_translations.py @@ -10,23 +10,25 @@ if settings.USE_I18N: def translation(language): global _translations if language not in _translations: - _translations[language] = DjangoTranslation(language, domain='dmoj-user') + _translations[language] = DjangoTranslation(language, domain="dmoj-user") return _translations[language] def do_translate(message, translation_function): """Copied from django.utils.translation.trans_real""" # str() is allowing a bytestring message to remain bytestring on Python 2 - eol_message = message.replace(str('\r\n'), str('\n')).replace(str('\r'), str('\n')) + eol_message = message.replace(str("\r\n"), str("\n")).replace( + str("\r"), str("\n") + ) if len(eol_message) == 0: # Returns an empty value of the corresponding type if an empty message # is given, instead of metadata, which is the default gettext behavior. - result = '' + result = "" else: translation_object = translation(get_language()) result = getattr(translation_object, translation_function)(eol_message) if not isinstance(result, six.text_type): - result = result.decode('utf-8') + result = result.decode("utf-8") if isinstance(message, SafeData): return mark_safe(result) @@ -34,7 +36,9 @@ if settings.USE_I18N: return result def gettext(message): - return do_translate(message, 'gettext') + return do_translate(message, "gettext") + else: + def gettext(message): return message diff --git a/judge/utils/camo.py b/judge/utils/camo.py index a3c90a5..025963f 100644 --- a/judge/utils/camo.py +++ b/judge/utils/camo.py @@ -10,39 +10,44 @@ class CamoClient(object): """Based on https://github.com/sionide21/camo-client""" def __init__(self, server, key, excluded=(), https=False): - self.server = server.rstrip('/') + self.server = server.rstrip("/") self.key = key self.https = https self.excluded = excluded def image_url(self, url): - return '%s/%s/%s' % (self.server, - hmac.new(utf8bytes(self.key), utf8bytes(url), sha1).hexdigest(), - utf8bytes(url).hex()) + return "%s/%s/%s" % ( + self.server, + hmac.new(utf8bytes(self.key), utf8bytes(url), sha1).hexdigest(), + utf8bytes(url).hex(), + ) def rewrite_url(self, url): if url.startswith(self.server) or url.startswith(self.excluded): return url - elif url.startswith(('http://', 'https://')): + elif url.startswith(("http://", "https://")): return self.image_url(url) - elif url.startswith('//'): - return self.rewrite_url(('https:' if self.https else 'http:') + url) + elif url.startswith("//"): + return self.rewrite_url(("https:" if self.https else "http:") + url) else: return url def update_tree(self, doc): - for img in doc.xpath('.//img'): - for attr in ('src', 'data-src'): + for img in doc.xpath(".//img"): + for attr in ("src", "data-src"): if img.get(attr): img.set(attr, self.rewrite_url(img.get(attr))) - for obj in doc.xpath('.//object'): - if obj.get('data'): - obj.set('data', self.rewrite_url(obj.get('data'))) + for obj in doc.xpath(".//object"): + if obj.get("data"): + obj.set("data", self.rewrite_url(obj.get("data"))) if settings.DMOJ_CAMO_URL and settings.DMOJ_CAMO_KEY: - client = CamoClient(settings.DMOJ_CAMO_URL, key=settings.DMOJ_CAMO_KEY, - excluded=settings.DMOJ_CAMO_EXCLUDE, - https=settings.DMOJ_CAMO_HTTPS) + client = CamoClient( + settings.DMOJ_CAMO_URL, + key=settings.DMOJ_CAMO_KEY, + excluded=settings.DMOJ_CAMO_EXCLUDE, + https=settings.DMOJ_CAMO_HTTPS, + ) else: client = None diff --git a/judge/utils/caniuse.py b/judge/utils/caniuse.py index bd4bb52..2414785 100644 --- a/judge/utils/caniuse.py +++ b/judge/utils/caniuse.py @@ -1,15 +1,17 @@ import requests from ua_parser import user_agent_parser -_SUPPORT_DATA = requests.get('https://raw.githubusercontent.com/Fyrd/caniuse/master/data.json').json()['data'] +_SUPPORT_DATA = requests.get( + "https://raw.githubusercontent.com/Fyrd/caniuse/master/data.json" +).json()["data"] -SUPPORT = 'y' -PARTIAL_SUPPORT = 'a' -UNSUPPORTED = 'n' -POLYFILL = 'p' -UNKNOWN = 'u' -PREFIX = 'x' -DISABLED = 'd' +SUPPORT = "y" +PARTIAL_SUPPORT = "a" +UNSUPPORTED = "n" +POLYFILL = "p" +UNKNOWN = "u" +PREFIX = "x" +DISABLED = "d" def safe_int(string): @@ -28,19 +30,19 @@ class BrowserFamily(object): max_support = UNKNOWN for version, support in data.items(): - if version == 'all': + if version == "all": self.max_support = support - elif '-' in version: - start, end = version.split('-') - start = tuple(map(int, start.split('.'))) - end = tuple(map(int, end.split('.'))) + (1e3000,) + elif "-" in version: + start, end = version.split("-") + start = tuple(map(int, start.split("."))) + end = tuple(map(int, end.split("."))) + (1e3000,) ranges.append((start, end, support)) if end > max_version: max_version = end max_support = support else: try: - version = tuple(map(int, version.split('.'))) + version = tuple(map(int, version.split("."))) except ValueError: pass else: @@ -59,7 +61,12 @@ class BrowserFamily(object): if version > self.max_version: return self.max_support - for key in ((int_major, int_minor, int_patch), (int_major, int_minor), (int_major,), major): + for key in ( + (int_major, int_minor, int_patch), + (int_major, int_minor), + (int_major,), + major, + ): try: return self._versions[key] except KeyError: @@ -75,7 +82,9 @@ class BrowserFamily(object): class Feat(object): def __init__(self, data): self._data = data - self._family = {name: BrowserFamily(data) for name, data in data['stats'].items()} + self._family = { + name: BrowserFamily(data) for name, data in data["stats"].items() + } def __getitem__(self, item): return self._family[item] @@ -97,31 +106,31 @@ class CanIUse(object): def __init__(self, ua): self._agent = user_agent_parser.Parse(ua) - os_family = self._agent['os']['family'] - browser_family = self._agent['user_agent']['family'] + os_family = self._agent["os"]["family"] + browser_family = self._agent["user_agent"]["family"] family = None - if os_family == 'Android': - if 'Firefox' in browser_family: - family = 'and_ff' - elif 'Chrome' in browser_family: - family = 'and_chr' - elif 'Android' in browser_family: - family = 'android' + if os_family == "Android": + if "Firefox" in browser_family: + family = "and_ff" + elif "Chrome" in browser_family: + family = "and_chr" + elif "Android" in browser_family: + family = "android" else: - if 'Edge' in browser_family: - family = 'edge' - elif 'Firefox' in browser_family: - family = 'firefox' - elif 'Chrome' in browser_family: - family = 'chrome' - elif 'IE' in browser_family: - family = 'ie' - elif 'Opera' in browser_family: - family = 'opera' - elif 'Safari' in browser_family: - family = 'safari' + if "Edge" in browser_family: + family = "edge" + elif "Firefox" in browser_family: + family = "firefox" + elif "Chrome" in browser_family: + family = "chrome" + elif "IE" in browser_family: + family = "ie" + elif "Opera" in browser_family: + family = "opera" + elif "Safari" in browser_family: + family = "safari" self._family = family @@ -134,12 +143,12 @@ class CanIUse(object): except KeyError: return UNKNOWN else: - ua = self._agent['user_agent'] - return stats.check(ua['major'], ua['minor'], ua['patch'])[0] + ua = self._agent["user_agent"] + return stats.check(ua["major"], ua["minor"], ua["patch"])[0] def __getattr__(self, attr): try: - feat = database[attr.replace('_', '-')] + feat = database[attr.replace("_", "-")] except KeyError: raise AttributeError(attr) else: diff --git a/judge/utils/celery.py b/judge/utils/celery.py index c905eae..a632ddb 100644 --- a/judge/utils/celery.py +++ b/judge/utils/celery.py @@ -12,11 +12,11 @@ class Progress: def _update_state(self): self.task.update_state( - state='PROGRESS', + state="PROGRESS", meta={ - 'done': self._done, - 'total': self._total, - 'stage': self._stage, + "done": self._done, + "total": self._total, + "stage": self._stage, }, ) @@ -54,12 +54,12 @@ class Progress: def task_status_url(result, message=None, redirect=None): args = {} if message: - args['message'] = message + args["message"] = message if redirect: - args['redirect'] = redirect - url = reverse('task_status', args=[result.id]) + args["redirect"] = redirect + url = reverse("task_status", args=[result.id]) if args: - url += '?' + urlencode(args) + url += "?" + urlencode(args) return url diff --git a/judge/utils/diggpaginator.py b/judge/utils/diggpaginator.py index 2832d2d..de9ee7d 100644 --- a/judge/utils/diggpaginator.py +++ b/judge/utils/diggpaginator.py @@ -4,10 +4,10 @@ from functools import reduce from django.core.paginator import InvalidPage, Page, Paginator __all__ = ( - 'InvalidPage', - 'ExPaginator', - 'DiggPaginator', - 'QuerySetDiggPaginator', + "InvalidPage", + "ExPaginator", + "DiggPaginator", + "QuerySetDiggPaginator", ) @@ -182,15 +182,17 @@ class DiggPaginator(ExPaginator): """ def __init__(self, *args, **kwargs): - self.body = kwargs.pop('body', 10) - self.tail = kwargs.pop('tail', 2) - self.align_left = kwargs.pop('align_left', False) - self.margin = kwargs.pop('margin', 4) # TODO: make the default relative to body? + self.body = kwargs.pop("body", 10) + self.tail = kwargs.pop("tail", 2) + self.align_left = kwargs.pop("align_left", False) + self.margin = kwargs.pop( + "margin", 4 + ) # TODO: make the default relative to body? # validate padding value max_padding = int(math.ceil(self.body / 2.0) - 1) - self.padding = kwargs.pop('padding', min(4, max_padding)) + self.padding = kwargs.pop("padding", min(4, max_padding)) if self.padding > max_padding: - raise ValueError('padding too large for body (max %d)' % max_padding) + raise ValueError("padding too large for body (max %d)" % max_padding) super(DiggPaginator, self).__init__(*args, **kwargs) def page(self, number, *args, **kwargs): @@ -202,13 +204,24 @@ class DiggPaginator(ExPaginator): number = int(number) # we know this will work # easier access - num_pages, body, tail, padding, margin = \ - self.num_pages, self.body, self.tail, self.padding, self.margin + num_pages, body, tail, padding, margin = ( + self.num_pages, + self.body, + self.tail, + self.padding, + self.margin, + ) # put active page in middle of main range - main_range = list(map(int, [ - math.floor(number - body / 2.0) + 1, # +1 = shift odd body to right - math.floor(number + body / 2.0)])) + main_range = list( + map( + int, + [ + math.floor(number - body / 2.0) + 1, # +1 = shift odd body to right + math.floor(number + body / 2.0), + ], + ) + ) # adjust bounds if main_range[0] < 1: main_range = list(map(abs(main_range[0] - 1).__add__, main_range)) @@ -249,7 +262,10 @@ class DiggPaginator(ExPaginator): # section, again. main_range = [1, num_pages] else: - main_range = [min(num_pages - body + 1, max(number - padding, main_range[0])), num_pages] + main_range = [ + min(num_pages - body + 1, max(number - padding, main_range[0])), + num_pages, + ] else: trailing = list(range(num_pages - tail + 1, num_pages + 1)) @@ -263,8 +279,10 @@ class DiggPaginator(ExPaginator): page.main_range = list(range(main_range[0], main_range[1] + 1)) page.leading_range = leading page.trailing_range = trailing - page.page_range = reduce(lambda x, y: x + ((x and y) and [False]) + y, - [page.leading_range, page.main_range, page.trailing_range]) + page.page_range = reduce( + lambda x, y: x + ((x and y) and [False]) + y, + [page.leading_range, page.main_range, page.trailing_range], + ) page.__class__ = DiggPage return page @@ -272,10 +290,16 @@ class DiggPaginator(ExPaginator): class DiggPage(Page): def __str__(self): - return " ... ".join(filter(None, [ - " ".join(map(str, self.leading_range)), - " ".join(map(str, self.main_range)), - " ".join(map(str, self.trailing_range))])) + return " ... ".join( + filter( + None, + [ + " ".join(map(str, self.leading_range)), + " ".join(map(str, self.main_range)), + " ".join(map(str, self.trailing_range)), + ], + ) + ) @property def num_pages(self): diff --git a/judge/utils/file_cache.py b/judge/utils/file_cache.py index 3864f59..3b19b4c 100644 --- a/judge/utils/file_cache.py +++ b/judge/utils/file_cache.py @@ -24,10 +24,10 @@ class HashFileCache(object): return os.path.join(self.root, hash, file) def get_url(self, hash, file): - return urljoin(self.url, '%s/%s' % (hash, file)) + return urljoin(self.url, "%s/%s" % (hash, file)) def read_file(self, hash, file): - return open(self.get_path(hash, file), 'rb') + return open(self.get_path(hash, file), "rb") def read_data(self, hash, file): with self.read_file(hash, file) as f: @@ -35,10 +35,10 @@ class HashFileCache(object): def cache_data(self, hash, file, data, url=True, gzip=True): if gzip and self.gzip: - with gzip_open(self.get_path(hash, file + '.gz'), 'wb') as f: + with gzip_open(self.get_path(hash, file + ".gz"), "wb") as f: f.write(data) - with open(self.get_path(hash, file), 'wb') as f: + with open(self.get_path(hash, file), "wb") as f: f.write(data) if url: diff --git a/judge/utils/fine_uploader.py b/judge/utils/fine_uploader.py index b38fcac..a9909a7 100644 --- a/judge/utils/fine_uploader.py +++ b/judge/utils/fine_uploader.py @@ -8,27 +8,25 @@ import os, os.path import tempfile import shutil -__all__ = ( - 'handle_upload', 'save_upload', 'FineUploadForm', 'FineUploadFileInput' -) +__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: + 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: + 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(): + with open(path, "wb+") as destination: + if hasattr(f, "multiple_chunks") and f.multiple_chunks(): for chunk in f.chunks(): destination.write(chunk) else: @@ -37,29 +35,35 @@ def save_upload(f, path): # 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') + 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']) + dest = os.path.join(dest_folder, fileattrs["qqfilename"]) # Chunked - if fileattrs.get('qqtotalparts') and int(fileattrs['qqtotalparts']) > 1: + 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'])) + 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'], + 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'])) + 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']): + + if post_upload and ( + not chunked or fileattrs["qqtotalparts"] - 1 == fileattrs["qqpartindex"] + ): post_upload() @@ -75,13 +79,16 @@ class FineUploadForm(forms.Form): class FineUploadFileInput(ClearableFileInput): - template_name = 'widgets/fine_uploader.html' + template_name = "widgets/fine_uploader.html" + def fine_uploader_id(self, name): - return name + '_' + 'fine_uploader' + 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 \ No newline at end of file + context["widget"].update( + { + "fine_uploader_id": self.fine_uploader_id(name), + } + ) + return context diff --git a/judge/utils/mathoid.py b/judge/utils/mathoid.py index a28357d..a7ceb24 100644 --- a/judge/utils/mathoid.py +++ b/judge/utils/mathoid.py @@ -12,25 +12,25 @@ from mistune import escape from judge.utils.file_cache import HashFileCache from judge.utils.unicode import utf8bytes, utf8text -logger = logging.getLogger('judge.mathoid') -reescape = re.compile(r'(?'), - ('&', '&'), - ('−', '-'), - ('≤', r'\le'), - ('≥', r'\ge'), - ('…', '...'), - (r'\lt', '<'), - (r'\gt', '>'), + ("\u2264", r"\le"), + ("\u2265", r"\ge"), + ("\u2026", "..."), + ("\u2212", "-"), + ("≤", r"\le"), + ("≥", r"\ge"), + ("<", "<"), + (">", ">"), + ("&", "&"), + ("−", "-"), + ("≤", r"\le"), + ("≥", r"\ge"), + ("…", "..."), + (r"\lt", "<"), + (r"\gt", ">"), ] @@ -41,15 +41,17 @@ def format_math(math): class MathoidMathParser(object): - types = ('svg', 'mml', 'tex', 'jax') + types = ("svg", "mml", "tex", "jax") def __init__(self, type): self.type = type self.mathoid_url = settings.MATHOID_URL - self.cache = HashFileCache(settings.MATHOID_CACHE_ROOT, - settings.MATHOID_CACHE_URL, - settings.MATHOID_GZIP) + self.cache = HashFileCache( + settings.MATHOID_CACHE_ROOT, + settings.MATHOID_CACHE_URL, + settings.MATHOID_GZIP, + ) mml_cache = settings.MATHOID_MML_CACHE self.mml_cache = mml_cache and caches[mml_cache] @@ -61,69 +63,80 @@ class MathoidMathParser(object): self.cache.create(hash) try: - response = requests.post(self.mathoid_url, data={ - 'q': reescape.sub(lambda m: '\\' + m.group(0), formula).encode('utf-8'), - 'type': 'tex' if formula.startswith(r'\displaystyle') else 'inline-tex', - }) + response = requests.post( + self.mathoid_url, + data={ + "q": reescape.sub(lambda m: "\\" + m.group(0), formula).encode( + "utf-8" + ), + "type": "tex" + if formula.startswith(r"\displaystyle") + else "inline-tex", + }, + ) response.raise_for_status() data = response.json() except requests.ConnectionError: - logger.exception('Failed to connect to mathoid for: %s', formula) + logger.exception("Failed to connect to mathoid for: %s", formula) return except requests.HTTPError as e: - logger.error('Mathoid failed to render: %s\n%s', formula, e.response.text) + logger.error("Mathoid failed to render: %s\n%s", formula, e.response.text) return except Exception: - logger.exception('Failed to connect to mathoid for: %s', formula) + logger.exception("Failed to connect to mathoid for: %s", formula) return - if not data['success']: - logger.error('Mathoid failure for: %s\n%s', formula, data) + if not data["success"]: + logger.error("Mathoid failure for: %s\n%s", formula, data) return - if any(i not in data for i in ('mml', 'png', 'svg', 'mathoidStyle')): - logger.error('Mathoid did not return required information (mml, png, svg, mathoidStyle needed):\n%s', data) + if any(i not in data for i in ("mml", "png", "svg", "mathoidStyle")): + logger.error( + "Mathoid did not return required information (mml, png, svg, mathoidStyle needed):\n%s", + data, + ) return - css = data['mathoidStyle'] - mml = data['mml'] + css = data["mathoidStyle"] + mml = data["mml"] result = { - 'css': css, 'mml': mml, - 'png': self.cache.cache_data(hash, 'png', bytearray(data['png']['data'])), - 'svg': self.cache.cache_data(hash, 'svg', data['svg'].encode('utf-8')), + "css": css, + "mml": mml, + "png": self.cache.cache_data(hash, "png", bytearray(data["png"]["data"])), + "svg": self.cache.cache_data(hash, "svg", data["svg"].encode("utf-8")), } - self.cache.cache_data(hash, 'mml', mml.encode('utf-8'), url=False, gzip=False) - self.cache.cache_data(hash, 'css', css.encode('utf-8'), url=False, gzip=False) + self.cache.cache_data(hash, "mml", mml.encode("utf-8"), url=False, gzip=False) + self.cache.cache_data(hash, "css", css.encode("utf-8"), url=False, gzip=False) return result def query_cache(self, hash): result = { - 'svg': self.cache.get_url(hash, 'svg'), - 'png': self.cache.get_url(hash, 'png'), + "svg": self.cache.get_url(hash, "svg"), + "png": self.cache.get_url(hash, "png"), } - key = 'mathoid:css:' + hash - css = result['css'] = self.css_cache.get(key) + key = "mathoid:css:" + hash + css = result["css"] = self.css_cache.get(key) if css is None: - css = result['css'] = self.cache.read_data(hash, 'css').decode('utf-8') + css = result["css"] = self.cache.read_data(hash, "css").decode("utf-8") self.css_cache.set(key, css, self.mml_cache_ttl) mml = None if self.mml_cache: - mml = result['mml'] = self.mml_cache.get('mathoid:mml:' + hash) + mml = result["mml"] = self.mml_cache.get("mathoid:mml:" + hash) if mml is None: - mml = result['mml'] = self.cache.read_data(hash, 'mml').decode('utf-8') + mml = result["mml"] = self.cache.read_data(hash, "mml").decode("utf-8") if self.mml_cache: - self.mml_cache.set('mathoid:mml:' + hash, mml, self.mml_cache_ttl) + self.mml_cache.set("mathoid:mml:" + hash, mml, self.mml_cache_ttl) return result def get_result(self, formula): - if self.type == 'tex': + if self.type == "tex": return hash = hashlib.sha1(utf8bytes(formula)).hexdigest() formula = utf8text(formula) - if self.cache.has_file(hash, 'css'): + if self.cache.has_file(hash, "css"): result = self.query_cache(hash) else: result = self.query_mathoid(formula, hash) @@ -131,55 +144,76 @@ class MathoidMathParser(object): if not result: return None - result['tex'] = formula - result['display'] = formula.startswith(r'\displaystyle') + result["tex"] = formula + result["display"] = formula.startswith(r"\displaystyle") return { - 'mml': self.output_mml, - 'msp': self.output_msp, - 'svg': self.output_svg, - 'jax': self.output_jax, - 'png': self.output_png, - 'raw': lambda x: x, + "mml": self.output_mml, + "msp": self.output_msp, + "svg": self.output_svg, + "jax": self.output_jax, + "png": self.output_png, + "raw": lambda x: x, }[self.type](result) def output_mml(self, result): - return result['mml'] + return result["mml"] def output_msp(self, result): # 100% MediaWiki compatibility. - return format_html('' - '' - '', - mark_safe(result['mml']), result['svg'], result['png'], result['css'], result['tex'], - ['inline', 'display'][result['display']]) + return format_html( + '' + '' + '', + mark_safe(result["mml"]), + result["svg"], + result["png"], + result["css"], + result["tex"], + ["inline", "display"][result["display"]], + ) def output_jax(self, result): - return format_html('' - '''{3}''' - '''''' - '', - result['svg'], result['png'], result['css'], result['tex'], - ['inline-math', 'display-math'][result['display']], ['~', '$$'][result['display']]) + return format_html( + '' + '''{3}""" + """""" + "", + result["svg"], + result["png"], + result["css"], + result["tex"], + ["inline-math", "display-math"][result["display"]], + ["~", "$$"][result["display"]], + ) def output_svg(self, result): - return format_html('{3}''', - result['svg'], result['png'], result['css'], result['tex'], - ['inline-math', 'display-math'][result['display']]) + return format_html( + '{3}""", + result["svg"], + result["png"], + result["css"], + result["tex"], + ["inline-math", "display-math"][result["display"]], + ) def output_png(self, result): - return format_html('{2}', - result['png'], result['css'], result['tex'], - ['inline-math', 'display-math'][result['display']]) + return format_html( + '{2}', + result["png"], + result["css"], + result["tex"], + ["inline-math", "display-math"][result["display"]], + ) def display_math(self, math): math = format_math(math) - return self.get_result(r'\displaystyle ' + math) or r'\[%s\]' % escape(math) + return self.get_result(r"\displaystyle " + math) or r"\[%s\]" % escape(math) def inline_math(self, math): math = format_math(math) - return self.get_result(math) or r'\(%s\)' % escape(math) + return self.get_result(math) or r"\(%s\)" % escape(math) diff --git a/judge/utils/opengraph.py b/judge/utils/opengraph.py index df016c2..a323c90 100644 --- a/judge/utils/opengraph.py +++ b/judge/utils/opengraph.py @@ -10,15 +10,15 @@ def generate_opengraph(cache_key, data, style): if metadata is None: description = None tree = reference(markdown(data, style)).tree - for p in tree.iterfind('.//p'): + for p in tree.iterfind(".//p"): text = p.text_content().strip() if text: description = text break if description: - for remove in (r'\[', r'\]', r'\(', r'\)'): - description = description.replace(remove, '') - img = tree.xpath('.//img') - metadata = truncatewords(description, 60), img[0].get('src') if img else None + for remove in (r"\[", r"\]", r"\(", r"\)"): + description = description.replace(remove, "") + img = tree.xpath(".//img") + metadata = truncatewords(description, 60), img[0].get("src") if img else None cache.set(cache_key, metadata, 86400) return metadata diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py index 6dc471c..aeb64cb 100644 --- a/judge/utils/problem_data.py +++ b/judge/utils/problem_data.py @@ -13,13 +13,18 @@ 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' +VALIDATOR_TEMPLATE_PATH = "validator_template/template.py" if os.altsep: - def split_path_first(path, repath=re.compile('[%s]' % re.escape(os.sep + os.altsep))): + + def split_path_first( + path, repath=re.compile("[%s]" % re.escape(os.sep + os.altsep)) + ): return repath.split(path, 1) + else: + def split_path_first(path): return path.split(os.sep, 1) @@ -31,8 +36,8 @@ class ProblemDataStorage(FileSystemStorage): def url(self, name): path = split_path_first(name) if len(path) != 2: - raise ValueError('This file is not accessible via a URL.') - return reverse('problem_data_file', args=path) + raise ValueError("This file is not accessible via a URL.") + return reverse("problem_data_file", args=path) def _save(self, name, content): if self.exists(name): @@ -66,8 +71,8 @@ class ProblemDataCompiler(object): batch = None def end_batch(): - if not batch['batched']: - raise ProblemDataError(_('Empty batches not allowed.')) + if not batch["batched"]: + raise ProblemDataError(_("Empty batches not allowed.")) cases.append(batch) def make_checker_for_validator(case): @@ -75,109 +80,123 @@ class ProblemDataCompiler(object): validator_path = split_path_first(case.custom_validator.name) if len(validator_path) != 2: - raise ProblemDataError(_('How did you corrupt the custom checker path?')) + raise ProblemDataError( + _("How did you corrupt the custom checker path?") + ) - checker = os.path.join(settings.DMOJ_PROBLEM_DATA_ROOT, - validator_path[0], - checker_name) + checker = os.path.join( + settings.DMOJ_PROBLEM_DATA_ROOT, validator_path[0], checker_name + ) validator_name = validator_path[1] shutil.copy(VALIDATOR_TEMPLATE_PATH, checker) # replace {{filecpp}} and {{problemid}} in checker file - filedata = open(checker, 'r').read() - filedata = filedata.replace('{{filecpp}}', "\'%s\'" % validator_name) - filedata = filedata.replace('{{problemid}}', "\'%s\'" % validator_path[0]) - open(checker, 'w').write(filedata) + filedata = open(checker, "r").read() + filedata = filedata.replace("{{filecpp}}", "'%s'" % validator_name) + filedata = filedata.replace("{{problemid}}", "'%s'" % validator_path[0]) + open(checker, "w").write(filedata) return checker_name def make_checker(case): - if (case.checker == 'custom'): + if case.checker == "custom": custom_checker_path = split_path_first(case.custom_checker.name) if len(custom_checker_path) != 2: - raise ProblemDataError(_('How did you corrupt the custom checker path?')) - return(custom_checker_path[1]) + raise ProblemDataError( + _("How did you corrupt the custom checker path?") + ) + return custom_checker_path[1] - if (case.checker == 'customval'): + if case.checker == "customval": return make_checker_for_validator(case) if case.checker_args: return { - 'name': case.checker, - 'args': json.loads(case.checker_args), + "name": case.checker, + "args": json.loads(case.checker_args), } return case.checker for i, case in enumerate(self.cases, 1): - if case.type == 'C': + if case.type == "C": data = {} if batch: case.points = None - case.is_pretest = batch['is_pretest'] + case.is_pretest = batch["is_pretest"] else: if case.points is None: - raise ProblemDataError(_('Points must be defined for non-batch case #%d.') % i) - data['is_pretest'] = case.is_pretest + raise ProblemDataError( + _("Points must be defined for non-batch case #%d.") % i + ) + data["is_pretest"] = case.is_pretest if not self.generator: if case.input_file not in self.files: - raise ProblemDataError(_('Input file for case %d does not exist: %s') % - (i, case.input_file)) + raise ProblemDataError( + _("Input file for case %d does not exist: %s") + % (i, case.input_file) + ) if case.output_file not in self.files: - raise ProblemDataError(_('Output file for case %d does not exist: %s') % - (i, case.output_file)) + raise ProblemDataError( + _("Output file for case %d does not exist: %s") + % (i, case.output_file) + ) if case.input_file: - data['in'] = case.input_file + data["in"] = case.input_file if case.output_file: - data['out'] = case.output_file + data["out"] = case.output_file if case.points is not None: - data['points'] = case.points + data["points"] = case.points if case.generator_args: - data['generator_args'] = case.generator_args.splitlines() + data["generator_args"] = case.generator_args.splitlines() if case.output_limit is not None: - data['output_limit_length'] = case.output_limit + data["output_limit_length"] = case.output_limit if case.output_prefix is not None: - data['output_prefix_length'] = case.output_prefix + data["output_prefix_length"] = case.output_prefix if case.checker: - data['checker'] = make_checker(case) + data["checker"] = make_checker(case) else: - case.checker_args = '' - case.save(update_fields=('checker_args', 'is_pretest')) - (batch['batched'] if batch else cases).append(data) - elif case.type == 'S': + case.checker_args = "" + case.save(update_fields=("checker_args", "is_pretest")) + (batch["batched"] if batch else cases).append(data) + elif case.type == "S": if batch: end_batch() if case.points is None: - raise ProblemDataError(_('Batch start case #%d requires points.') % i) + raise ProblemDataError( + _("Batch start case #%d requires points.") % i + ) batch = { - 'points': case.points, - 'batched': [], - 'is_pretest': case.is_pretest, + "points": case.points, + "batched": [], + "is_pretest": case.is_pretest, } if case.generator_args: - batch['generator_args'] = case.generator_args.splitlines() + batch["generator_args"] = case.generator_args.splitlines() if case.output_limit is not None: - batch['output_limit_length'] = case.output_limit + batch["output_limit_length"] = case.output_limit if case.output_prefix is not None: - batch['output_prefix_length'] = case.output_prefix + batch["output_prefix_length"] = case.output_prefix if case.checker: - batch['checker'] = make_checker(case) + batch["checker"] = make_checker(case) else: - case.checker_args = '' - case.input_file = '' - case.output_file = '' - case.save(update_fields=('checker_args', 'input_file', 'output_file')) - elif case.type == 'E': + case.checker_args = "" + case.input_file = "" + case.output_file = "" + case.save(update_fields=("checker_args", "input_file", "output_file")) + elif case.type == "E": if not batch: - raise ProblemDataError(_('Attempt to end batch outside of one in case #%d') % i) - case.is_pretest = batch['is_pretest'] - case.input_file = '' - case.output_file = '' - case.generator_args = '' - case.checker = '' - case.checker_args = '' + raise ProblemDataError( + _("Attempt to end batch outside of one in case #%d") % i + ) + case.is_pretest = batch["is_pretest"] + case.input_file = "" + case.output_file = "" + case.generator_args = "" + case.checker = "" + case.checker_args = "" case.save() end_batch() batch = None @@ -189,44 +208,44 @@ class ProblemDataCompiler(object): if self.data.zipfile: zippath = split_path_first(self.data.zipfile.name) if len(zippath) != 2: - raise ProblemDataError(_('How did you corrupt the zip path?')) - init['archive'] = zippath[1] + raise ProblemDataError(_("How did you corrupt the zip path?")) + init["archive"] = zippath[1] if self.generator: generator_path = split_path_first(self.generator.name) if len(generator_path) != 2: - raise ProblemDataError(_('How did you corrupt the generator path?')) - init['generator'] = generator_path[1] + raise ProblemDataError(_("How did you corrupt the generator path?")) + init["generator"] = generator_path[1] - pretests = [case for case in cases if case['is_pretest']] + pretests = [case for case in cases if case["is_pretest"]] for case in cases: - del case['is_pretest'] + del case["is_pretest"] if pretests: - init['pretest_test_cases'] = pretests + init["pretest_test_cases"] = pretests if cases: - init['test_cases'] = cases + init["test_cases"] = cases if self.data.output_limit is not None: - init['output_limit_length'] = self.data.output_limit + init["output_limit_length"] = self.data.output_limit if self.data.output_prefix is not None: - init['output_prefix_length'] = self.data.output_prefix + init["output_prefix_length"] = self.data.output_prefix if self.data.checker: - if self.data.checker == 'interact': - init['interactive'] = { - 'files': split_path_first(self.data.interactive_judge.name)[1], - 'feedback': True + if self.data.checker == "interact": + init["interactive"] = { + "files": split_path_first(self.data.interactive_judge.name)[1], + "feedback": True, } - init['unbuffered'] = True + init["unbuffered"] = True else: - init['checker'] = make_checker(self.data) + init["checker"] = make_checker(self.data) else: - self.data.checker_args = '' + self.data.checker_args = "" return init def compile(self): from judge.models import problem_data_storage - yml_file = '%s/init.yml' % self.problem.code + yml_file = "%s/init.yml" % self.problem.code try: init = yaml.safe_dump(self.make_init()) except ProblemDataError as e: @@ -234,7 +253,7 @@ class ProblemDataCompiler(object): self.data.save() problem_data_storage.delete(yml_file) else: - self.data.feedback = '' + self.data.feedback = "" self.data.save() problem_data_storage.save(yml_file, ContentFile(init)) @@ -245,26 +264,27 @@ class ProblemDataCompiler(object): def get_visible_content(data): - data = data or b'' - data = data.replace(b'\r\n', b'\r').replace(b'\r', b'\n') + data = data or b"" + data = data.replace(b"\r\n", b"\r").replace(b"\r", b"\n") - data = data.decode('utf-8') + data = data.decode("utf-8") - if (len(data) > settings.TESTCASE_VISIBLE_LENGTH): - data = data[:settings.TESTCASE_VISIBLE_LENGTH] - data += '.' * 3 + 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)) + 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) @@ -274,33 +294,33 @@ def get_problem_case(problem, files): if not uncached_files: return result - archive_path = os.path.join(settings.DMOJ_PROBLEM_DATA_ROOT, - str(problem.data_files.zipfile)) + 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) + raise Exception('archive file "%s" does not exist' % archive_path) try: - archive = zipfile.ZipFile(archive_path, 'r') + 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)) + 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') + s.decode("utf-8") break except UnicodeDecodeError: next_char = f.read(1) if next_char: s += next_char else: - raise Exception('File %s is not able to decode in utf-8' % file) + raise Exception("File %s is not able to decode in utf-8" % file) qs = get_visible_content(s) cache.set(cache_key, qs, 86400) result[file] = qs - return result \ No newline at end of file + return result diff --git a/judge/utils/problems.py b/judge/utils/problems.py index aacb916..73d53d0 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -11,79 +11,120 @@ from django.utils.translation import gettext as _, gettext_noop from judge.models import Problem, Submission -__all__ = ['contest_completed_ids', 'get_result_data', 'user_completed_ids', 'user_authored_ids', 'user_editable_ids'] +__all__ = [ + "contest_completed_ids", + "get_result_data", + "user_completed_ids", + "user_authored_ids", + "user_editable_ids", +] def user_authored_ids(profile): - result = set(Problem.objects.filter(authors=profile).values_list('id', flat=True)) + result = set(Problem.objects.filter(authors=profile).values_list("id", flat=True)) return result def user_editable_ids(profile): - result = set((Problem.objects.filter(authors=profile) | Problem.objects.filter(curators=profile)) - .values_list('id', flat=True)) + result = set( + ( + Problem.objects.filter(authors=profile) + | Problem.objects.filter(curators=profile) + ).values_list("id", flat=True) + ) return result def contest_completed_ids(participation): - key = 'contest_complete:%d' % participation.id + key = "contest_complete:%d" % participation.id result = cache.get(key) if result is None: - result = set(participation.submissions.filter(submission__result='AC', points=F('problem__points')) - .values_list('problem__problem__id', flat=True).distinct()) + result = set( + participation.submissions.filter( + submission__result="AC", points=F("problem__points") + ) + .values_list("problem__problem__id", flat=True) + .distinct() + ) cache.set(key, result, 86400) return result def user_completed_ids(profile): - key = 'user_complete:%d' % profile.id + key = "user_complete:%d" % profile.id result = cache.get(key) if result is None: - result = set(Submission.objects.filter(user=profile, result='AC', points=F('problem__points')) - .values_list('problem_id', flat=True).distinct()) + result = set( + Submission.objects.filter( + user=profile, result="AC", points=F("problem__points") + ) + .values_list("problem_id", flat=True) + .distinct() + ) cache.set(key, result, 86400) return result def contest_attempted_ids(participation): - key = 'contest_attempted:%s' % participation.id + key = "contest_attempted:%s" % participation.id result = cache.get(key) if result is None: - result = {id: {'achieved_points': points, 'max_points': max_points} - for id, max_points, points in (participation.submissions - .values_list('problem__problem__id', 'problem__points') - .annotate(points=Max('points')) - .filter(points__lt=F('problem__points')))} + result = { + id: {"achieved_points": points, "max_points": max_points} + for id, max_points, points in ( + participation.submissions.values_list( + "problem__problem__id", "problem__points" + ) + .annotate(points=Max("points")) + .filter(points__lt=F("problem__points")) + ) + } cache.set(key, result, 86400) return result def user_attempted_ids(profile): - key = 'user_attempted:%s' % profile.id + key = "user_attempted:%s" % profile.id result = cache.get(key) if result is None: - result = {id: {'achieved_points': points, 'max_points': max_points} - for id, max_points, points in (Submission.objects.filter(user=profile) - .values_list('problem__id', 'problem__points') - .annotate(points=Max('points')) - .filter(points__lt=F('problem__points')))} + result = { + id: {"achieved_points": points, "max_points": max_points} + for id, max_points, points in ( + Submission.objects.filter(user=profile) + .values_list("problem__id", "problem__points") + .annotate(points=Max("points")) + .filter(points__lt=F("problem__points")) + ) + } cache.set(key, result, 86400) return result def _get_result_data(results): return { - 'categories': [ + "categories": [ # Using gettext_noop here since this will be tacked into the cache, so it must be language neutral. # The caller, SubmissionList.get_result_data will run ugettext on the name. - {'code': 'AC', 'name': gettext_noop('Accepted'), 'count': results['AC']}, - {'code': 'WA', 'name': gettext_noop('Wrong'), 'count': results['WA']}, - {'code': 'CE', 'name': gettext_noop('Compile Error'), 'count': results['CE']}, - {'code': 'TLE', 'name': gettext_noop('Timeout'), 'count': results['TLE']}, - {'code': 'ERR', 'name': gettext_noop('Error'), - 'count': results['MLE'] + results['OLE'] + results['IR'] + results['RTE'] + results['AB'] + results['IE']}, + {"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]}, + {"code": "WA", "name": gettext_noop("Wrong"), "count": results["WA"]}, + { + "code": "CE", + "name": gettext_noop("Compile Error"), + "count": results["CE"], + }, + {"code": "TLE", "name": gettext_noop("Timeout"), "count": results["TLE"]}, + { + "code": "ERR", + "name": gettext_noop("Error"), + "count": results["MLE"] + + results["OLE"] + + results["IR"] + + results["RTE"] + + results["AB"] + + results["IE"], + }, ], - 'total': sum(results.values()), + "total": sum(results.values()), } @@ -93,8 +134,16 @@ def get_result_data(*args, **kwargs): if kwargs: raise ValueError(_("Can't pass both queryset and keyword filters")) else: - submissions = Submission.objects.filter(**kwargs) if kwargs is not None else Submission.objects - raw = submissions.values('result').annotate(count=Count('result')).values_list('result', 'count') + submissions = ( + Submission.objects.filter(**kwargs) + if kwargs is not None + else Submission.objects + ) + raw = ( + submissions.values("result") + .annotate(count=Count("result")) + .values_list("result", "count") + ) return _get_result_data(defaultdict(int, raw)) @@ -102,48 +151,73 @@ def editable_problems(user, profile=None): subquery = Problem.objects.all() if profile is None: profile = user.profile - if not user.has_perm('judge.edit_all_problem'): + if not user.has_perm("judge.edit_all_problem"): subfilter = Q(authors__id=profile.id) | Q(curators__id=profile.id) - if user.has_perm('judge.edit_public_problem'): + if user.has_perm("judge.edit_public_problem"): subfilter |= Q(is_public=True) subquery = subquery.filter(subfilter) return subquery def hot_problems(duration, limit): - cache_key = 'hot_problems:%d:%d' % (duration.total_seconds(), limit) + cache_key = "hot_problems:%d:%d" % (duration.total_seconds(), limit) qs = cache.get(cache_key) if qs is None: - qs = Problem.get_public_problems() \ - .filter(submission__date__gt=timezone.now() - duration) - qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True) + qs = Problem.get_public_problems().filter( + submission__date__gt=timezone.now() - duration + ) + qs0 = ( + qs.annotate(k=Count("submission__user", distinct=True)) + .order_by("-k") + .values_list("k", flat=True) + ) if not qs0: return [] # make this an aggregate mx = float(qs0[0]) - qs = qs.annotate(unique_user_count=Count('submission__user', distinct=True)) + qs = qs.annotate(unique_user_count=Count("submission__user", distinct=True)) # fix braindamage in excluding CE - qs = qs.annotate(submission_volume=Count(Case( - When(submission__result='AC', then=1), - When(submission__result='WA', then=1), - When(submission__result='IR', then=1), - When(submission__result='RTE', then=1), - When(submission__result='TLE', then=1), - When(submission__result='OLE', then=1), - output_field=FloatField(), - ))) - qs = qs.annotate(ac_volume=Count(Case( - When(submission__result='AC', then=1), - output_field=FloatField(), - ))) + qs = qs.annotate( + submission_volume=Count( + Case( + When(submission__result="AC", then=1), + When(submission__result="WA", then=1), + When(submission__result="IR", then=1), + When(submission__result="RTE", then=1), + When(submission__result="TLE", then=1), + When(submission__result="OLE", then=1), + output_field=FloatField(), + ) + ) + ) + qs = qs.annotate( + ac_volume=Count( + Case( + When(submission__result="AC", then=1), + output_field=FloatField(), + ) + ) + ) qs = qs.filter(unique_user_count__gt=max(mx / 3.0, 1)) - qs = qs.annotate(ordering=ExpressionWrapper( - 0.02 * F('points') * (0.4 * F('ac_volume') / F('submission_volume') + 0.6 * F('ac_rate')) + - 100 * e ** (F('unique_user_count') / mx), output_field=FloatField(), - )).order_by('-ordering').defer('description')[:limit] + qs = ( + qs.annotate( + ordering=ExpressionWrapper( + 0.02 + * F("points") + * ( + 0.4 * F("ac_volume") / F("submission_volume") + + 0.6 * F("ac_rate") + ) + + 100 * e ** (F("unique_user_count") / mx), + output_field=FloatField(), + ) + ) + .order_by("-ordering") + .defer("description")[:limit] + ) cache.set(cache_key, qs, 900) - return qs \ No newline at end of file + return qs diff --git a/judge/utils/pwned.py b/judge/utils/pwned.py index fa3df63..0070003 100644 --- a/judge/utils/pwned.py +++ b/judge/utils/pwned.py @@ -47,7 +47,7 @@ from judge.utils.unicode import utf8bytes log = logging.getLogger(__name__) -API_ENDPOINT = 'https://api.pwnedpasswords.com/range/{}' +API_ENDPOINT = "https://api.pwnedpasswords.com/range/{}" REQUEST_TIMEOUT = 2.0 # 2 seconds @@ -61,19 +61,19 @@ def _get_pwned(prefix): url=API_ENDPOINT.format(prefix), timeout=getattr( settings, - 'PWNED_PASSWORDS_API_TIMEOUT', + "PWNED_PASSWORDS_API_TIMEOUT", REQUEST_TIMEOUT, ), ) response.raise_for_status() except requests.RequestException: # Gracefully handle timeouts and HTTP error response codes. - log.warning('Skipped Pwned Passwords check due to error', exc_info=True) + log.warning("Skipped Pwned Passwords check due to error", exc_info=True) return None results = {} for line in response.text.splitlines(): - line_suffix, _, times = line.partition(':') + line_suffix, _, times = line.partition(":") results[line_suffix] = int(times) return results @@ -84,7 +84,7 @@ def pwned_password(password): Checks a password against the Pwned Passwords database. """ if not isinstance(password, string_types): - raise TypeError('Password values to check must be strings.') + raise TypeError("Password values to check must be strings.") password_hash = hashlib.sha1(utf8bytes(password)).hexdigest().upper() prefix, suffix = password_hash[:5], password_hash[5:] results = _get_pwned(prefix) @@ -98,8 +98,9 @@ class PwnedPasswordsValidator(object): """ Password validator which checks the Pwned Passwords database. """ + DEFAULT_HELP_MESSAGE = _("Your password can't be a commonly used password.") - DEFAULT_PWNED_MESSAGE = _('This password is too common.') + DEFAULT_PWNED_MESSAGE = _("This password is too common.") def __init__(self, error_message=None, help_message=None): self.help_message = help_message or self.DEFAULT_HELP_MESSAGE @@ -111,8 +112,8 @@ class PwnedPasswordsValidator(object): else: singular, plural = error_message self.error_message = { - 'singular': singular, - 'plural': plural, + "singular": singular, + "plural": plural, } def validate(self, password, user=None): @@ -125,12 +126,12 @@ class PwnedPasswordsValidator(object): elif amount: raise ValidationError( ungettext( - self.error_message['singular'], - self.error_message['plural'], + self.error_message["singular"], + self.error_message["plural"], amount, ), - params={'amount': amount}, - code='pwned_password', + params={"amount": amount}, + code="pwned_password", ) def get_help_text(self): diff --git a/judge/utils/ranker.py b/judge/utils/ranker.py index 52f0e4c..3f15e62 100644 --- a/judge/utils/ranker.py +++ b/judge/utils/ranker.py @@ -1,7 +1,7 @@ from operator import attrgetter -def ranker(iterable, key=attrgetter('points'), rank=0): +def ranker(iterable, key=attrgetter("points"), rank=0): delta = 1 last = None for item in iterable: @@ -12,4 +12,3 @@ def ranker(iterable, key=attrgetter('points'), rank=0): delta += 1 yield rank, item last = key(item) - diff --git a/judge/utils/raw_sql.py b/judge/utils/raw_sql.py index edbca4c..3111e3b 100644 --- a/judge/utils/raw_sql.py +++ b/judge/utils/raw_sql.py @@ -10,13 +10,18 @@ from django.utils import six from judge.utils.cachedict import CacheDict -def unique_together_left_join(queryset, model, link_field_name, filter_field_name, filter_value, parent_model=None): +def unique_together_left_join( + queryset, model, link_field_name, filter_field_name, filter_value, parent_model=None +): link_field = copy(model._meta.get_field(link_field_name).remote_field) filter_field = model._meta.get_field(filter_field_name) def restrictions(where_class, alias, related_alias): cond = where_class() - cond.add(filter_field.get_lookup('exact')(filter_field.get_col(alias), filter_value), 'AND') + cond.add( + filter_field.get_lookup("exact")(filter_field.get_col(alias), filter_value), + "AND", + ) return cond link_field.get_extra_restriction = restrictions @@ -25,17 +30,36 @@ def unique_together_left_join(queryset, model, link_field_name, filter_field_nam parent_alias = parent_model._meta.db_table else: parent_alias = queryset.query.get_initial_alias() - return queryset.query.join(Join(model._meta.db_table, parent_alias, None, LOUTER, link_field, True)) + return queryset.query.join( + Join(model._meta.db_table, parent_alias, None, LOUTER, link_field, True) + ) class RawSQLJoin(Join): - def __init__(self, subquery, subquery_params, parent_alias, table_alias, join_type, join_field, nullable, - filtered_relation=None): + def __init__( + self, + subquery, + subquery_params, + parent_alias, + table_alias, + join_type, + join_field, + nullable, + filtered_relation=None, + ): self.subquery_params = subquery_params - super().__init__(subquery, parent_alias, table_alias, join_type, join_field, nullable, filtered_relation) + super().__init__( + subquery, + parent_alias, + table_alias, + join_type, + join_field, + nullable, + filtered_relation, + ) def as_sql(self, compiler, connection): - compiler.quote_cache[self.table_name] = '(%s)' % self.table_name + compiler.quote_cache[self.table_name] = "(%s)" % self.table_name sql, params = super().as_sql(compiler, connection) return sql, self.subquery_params + params @@ -51,13 +75,23 @@ class FakeJoinField: pass -def join_sql_subquery(queryset, subquery, params, join_fields, alias, join_type=INNER, parent_model=None): +def join_sql_subquery( + queryset, subquery, params, join_fields, alias, join_type=INNER, parent_model=None +): if parent_model is not None: parent_alias = parent_model._meta.db_table else: parent_alias = queryset.query.get_initial_alias() queryset.query.external_aliases.add(alias) - join = RawSQLJoin(subquery, params, parent_alias, alias, join_type, FakeJoinField(join_fields), join_type == LOUTER) + join = RawSQLJoin( + subquery, + params, + parent_alias, + alias, + join_type, + FakeJoinField(join_fields), + join_type == LOUTER, + ) queryset.query.join(join) join.table_alias = alias @@ -68,7 +102,7 @@ def RawSQLColumn(model, field=None): model = field.model if isinstance(field, six.string_types): field = model._meta.get_field(field) - return RawSQL('%s.%s' % (model._meta.db_table, field.get_attname_column()[1]), ()) + return RawSQL("%s.%s" % (model._meta.db_table, field.get_attname_column()[1]), ()) def make_straight_join_query(QueryType): @@ -77,7 +111,7 @@ def make_straight_join_query(QueryType): alias = super().join(join, *args, **kwargs) join = self.alias_map[alias] if join.join_type == INNER: - join.join_type = 'STRAIGHT_JOIN' + join.join_type = "STRAIGHT_JOIN" return alias return Query @@ -87,7 +121,7 @@ straight_join_cache = CacheDict(make_straight_join_query) def use_straight_join(queryset): - if connections[queryset.db].vendor != 'mysql': + if connections[queryset.db].vendor != "mysql": return try: cloner = queryset.query.chain diff --git a/judge/utils/recaptcha.py b/judge/utils/recaptcha.py index 331a12d..9be7e5f 100644 --- a/judge/utils/recaptcha.py +++ b/judge/utils/recaptcha.py @@ -6,6 +6,7 @@ except ImportError: ReCaptchaWidget = None else: from django.conf import settings - if not hasattr(settings, 'RECAPTCHA_PRIVATE_KEY'): + + if not hasattr(settings, "RECAPTCHA_PRIVATE_KEY"): ReCaptchaField = None ReCaptchaWidget = None diff --git a/judge/utils/stats.py b/judge/utils/stats.py index ba923aa..6c79d8c 100644 --- a/judge/utils/stats.py +++ b/judge/utils/stats.py @@ -1,10 +1,30 @@ from operator import itemgetter -__all__ = ('chart_colors', 'highlight_colors', 'get_pie_chart', 'get_bar_chart') +__all__ = ("chart_colors", "highlight_colors", "get_pie_chart", "get_bar_chart") -chart_colors = [0x3366CC, 0xDC3912, 0xFF9900, 0x109618, 0x990099, 0x3B3EAC, 0x0099C6, 0xDD4477, 0x66AA00, 0xB82E2E, - 0x316395, 0x994499, 0x22AA99, 0xAAAA11, 0x6633CC, 0xE67300, 0x8B0707, 0x329262, 0x5574A6, 0x3B3EAC] +chart_colors = [ + 0x3366CC, + 0xDC3912, + 0xFF9900, + 0x109618, + 0x990099, + 0x3B3EAC, + 0x0099C6, + 0xDD4477, + 0x66AA00, + 0xB82E2E, + 0x316395, + 0x994499, + 0x22AA99, + 0xAAAA11, + 0x6633CC, + 0xE67300, + 0x8B0707, + 0x329262, + 0x5574A6, + 0x3B3EAC, +] highlight_colors = [] @@ -13,25 +33,26 @@ highlight_colors = [] def _highlight_colors(): for color in chart_colors: r, g, b = color >> 16, (color >> 8) & 0xFF, color & 0xFF - highlight_colors.append('#%02X%02X%02X' % (min(int(r * 1.2), 255), - min(int(g * 1.2), 255), - min(int(b * 1.2), 255))) + highlight_colors.append( + "#%02X%02X%02X" + % (min(int(r * 1.2), 255), min(int(g * 1.2), 255), min(int(b * 1.2), 255)) + ) _highlight_colors() -chart_colors = list(map('#%06X'.__mod__, chart_colors)) +chart_colors = list(map("#%06X".__mod__, chart_colors)) def get_pie_chart(data): return { - 'labels': list(map(itemgetter(0), data)), - 'datasets': [ + "labels": list(map(itemgetter(0), data)), + "datasets": [ { - 'backgroundColor': chart_colors, - 'highlightBackgroundColor': highlight_colors, - 'data': list(map(itemgetter(1), data)), + "backgroundColor": chart_colors, + "highlightBackgroundColor": highlight_colors, + "data": list(map(itemgetter(1), data)), }, ], } @@ -39,30 +60,39 @@ def get_pie_chart(data): def get_bar_chart(data, **kwargs): return { - 'labels': list(map(itemgetter(0), data)), - 'datasets': [ + "labels": 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)), + "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)), }, ], } + def get_histogram(data, **kwargs): return { - 'labels': [round(i, 1) for i in list(map(itemgetter(0), data))], - 'datasets': [ + "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)), + "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)), }, ], - } \ No newline at end of file + } diff --git a/judge/utils/subscription.py b/judge/utils/subscription.py index 1129b0d..883d905 100644 --- a/judge/utils/subscription.py +++ b/judge/utils/subscription.py @@ -1,8 +1,10 @@ from django.conf import settings -if 'newsletter' in settings.INSTALLED_APPS: +if "newsletter" in settings.INSTALLED_APPS: from newsletter.models import Subscription else: Subscription = None -newsletter_id = None if Subscription is None else settings.DMOJ_NEWSLETTER_ID_ON_REGISTER +newsletter_id = ( + None if Subscription is None else settings.DMOJ_NEWSLETTER_ID_ON_REGISTER +) diff --git a/judge/utils/texoid.py b/judge/utils/texoid.py index d668066..cb51169 100644 --- a/judge/utils/texoid.py +++ b/judge/utils/texoid.py @@ -10,16 +10,16 @@ from django.core.cache import caches from judge.utils.file_cache import HashFileCache from judge.utils.unicode import utf8bytes -logger = logging.getLogger('judge.texoid') +logger = logging.getLogger("judge.texoid") -TEXOID_ENABLED = hasattr(settings, 'TEXOID_URL') +TEXOID_ENABLED = hasattr(settings, "TEXOID_URL") class TexoidRenderer(object): def __init__(self): - self.cache = HashFileCache(settings.TEXOID_CACHE_ROOT, - settings.TEXOID_CACHE_URL, - settings.TEXOID_GZIP) + self.cache = HashFileCache( + settings.TEXOID_CACHE_ROOT, settings.TEXOID_CACHE_URL, settings.TEXOID_GZIP + ) self.meta_cache = caches[settings.TEXOID_META_CACHE] self.meta_cache_ttl = settings.TEXOID_META_CACHE_TTL @@ -27,59 +27,69 @@ class TexoidRenderer(object): self.cache.create(hash) try: - response = requests.post(settings.TEXOID_URL, data=utf8bytes(document), headers={ - 'Content-Type': 'application/x-tex', - }) + response = requests.post( + settings.TEXOID_URL, + data=utf8bytes(document), + headers={ + "Content-Type": "application/x-tex", + }, + ) response.raise_for_status() except requests.HTTPError as e: if e.response.status == 400: - logger.error('Texoid failed to render: %s\n%s', document, e.response.text) + logger.error( + "Texoid failed to render: %s\n%s", document, e.response.text + ) else: - logger.exception('Failed to connect to texoid for: %s', document) + logger.exception("Failed to connect to texoid for: %s", document) return except Exception: - logger.exception('Failed to connect to texoid for: %s', document) + logger.exception("Failed to connect to texoid for: %s", document) return try: data = response.json() except ValueError: - logger.exception('Invalid texoid response for: %s\n%s', document, response.text) + logger.exception( + "Invalid texoid response for: %s\n%s", document, response.text + ) return - if not data['success']: - logger.error('Texoid failure for: %s\n%s', document, data) - return {'error': data['error']} + if not data["success"]: + logger.error("Texoid failure for: %s\n%s", document, data) + return {"error": data["error"]} - meta = data['meta'] - self.cache.cache_data(hash, 'meta', utf8bytes(json.dumps(meta)), url=False, gzip=False) + meta = data["meta"] + self.cache.cache_data( + hash, "meta", utf8bytes(json.dumps(meta)), url=False, gzip=False + ) result = { - 'png': self.cache.cache_data(hash, 'png', b64decode(data['png'])), - 'svg': self.cache.cache_data(hash, 'svg', data['svg'].encode('utf-8')), - 'meta': meta, + "png": self.cache.cache_data(hash, "png", b64decode(data["png"])), + "svg": self.cache.cache_data(hash, "svg", data["svg"].encode("utf-8")), + "meta": meta, } return result def query_cache(self, hash): result = { - 'svg': self.cache.get_url(hash, 'svg'), - 'png': self.cache.get_url(hash, 'png'), + "svg": self.cache.get_url(hash, "svg"), + "png": self.cache.get_url(hash, "png"), } - key = 'texoid:meta:' + hash + key = "texoid:meta:" + hash cached_meta = self.meta_cache.get(key) if cached_meta is None: - cached_meta = json.loads(self.cache.read_data(hash, 'meta').decode('utf-8')) + cached_meta = json.loads(self.cache.read_data(hash, "meta").decode("utf-8")) self.meta_cache.set(key, cached_meta, self.meta_cache_ttl) - result['meta'] = cached_meta + result["meta"] = cached_meta return result def get_result(self, formula): hash = hashlib.sha1(utf8bytes(formula)).hexdigest() - if self.cache.has_file(hash, 'svg'): + if self.cache.has_file(hash, "svg"): return self.query_cache(hash) else: return self.query_texoid(formula, hash) diff --git a/judge/utils/tickets.py b/judge/utils/tickets.py index 1743db4..b630e60 100644 --- a/judge/utils/tickets.py +++ b/judge/utils/tickets.py @@ -12,6 +12,10 @@ def own_ticket_filter(profile_id): def filter_visible_tickets(queryset, user, profile=None): if profile is None: profile = user.profile - return queryset.filter(own_ticket_filter(profile.id) | - Q(content_type=ContentType.objects.get_for_model(Problem), - object_id__in=editable_problems(user, profile))).distinct() + return queryset.filter( + own_ticket_filter(profile.id) + | Q( + content_type=ContentType.objects.get_for_model(Problem), + object_id__in=editable_problems(user, profile), + ) + ).distinct() diff --git a/judge/utils/timedelta.py b/judge/utils/timedelta.py index 292caeb..adf3330 100644 --- a/judge/utils/timedelta.py +++ b/judge/utils/timedelta.py @@ -3,7 +3,7 @@ import datetime from django.utils.translation import npgettext, pgettext, ungettext -def nice_repr(timedelta, display='long', sep=', '): +def nice_repr(timedelta, display="long", sep=", "): """ Turns a datetime.timedelta object into a nice string repr. @@ -16,7 +16,9 @@ def nice_repr(timedelta, display='long', sep=', '): '1d, 1s' """ - assert isinstance(timedelta, datetime.timedelta), 'First argument must be a timedelta.' + assert isinstance( + timedelta, datetime.timedelta + ), "First argument must be a timedelta." result = [] @@ -26,65 +28,94 @@ def nice_repr(timedelta, display='long', sep=', '): minutes = (timedelta.seconds % 3600) // 60 seconds = timedelta.seconds % 60 - if display == 'simple-no-seconds': + if display == "simple-no-seconds": days += weeks * 7 if days: if hours or minutes: - return '%d day%s %d:%02d' % (days, 's'[days == 1:], hours, minutes) - return '%d day%s' % (days, 's'[days == 1:]) + return "%d day%s %d:%02d" % (days, "s"[days == 1 :], hours, minutes) + return "%d day%s" % (days, "s"[days == 1 :]) else: - return '%d:%02d' % (hours, minutes) - elif display == 'sql': + return "%d:%02d" % (hours, minutes) + elif display == "sql": days += weeks * 7 - return '%d %02d:%02d:%02d' % (days, hours, minutes, seconds) - elif display == 'simple': + return "%d %02d:%02d:%02d" % (days, hours, minutes, seconds) + elif display == "simple": days += weeks * 7 if days: - return '%d day%s %02d:%02d:%02d' % (days, 's'[days == 1:], hours, minutes, seconds) + return "%d day%s %02d:%02d:%02d" % ( + days, + "s"[days == 1 :], + hours, + minutes, + seconds, + ) else: - return '%02d:%02d:%02d' % (hours, minutes, seconds) - elif display == 'localized': + return "%02d:%02d:%02d" % (hours, minutes, seconds) + elif display == "localized": days += weeks * 7 if days: - return npgettext('time format with day', '%d day %h:%m:%s', '%d days %h:%m:%s', days) \ - .replace('%d', str(days)).replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) \ - .replace('%s', '%02d' % seconds) + return ( + npgettext( + "time format with day", "%d day %h:%m:%s", "%d days %h:%m:%s", days + ) + .replace("%d", str(days)) + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + .replace("%s", "%02d" % seconds) + ) else: - return pgettext('time format without day', '%h:%m:%s') \ - .replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes).replace('%s', '%02d' % seconds) - elif display == 'localized-no-seconds': + return ( + pgettext("time format without day", "%h:%m:%s") + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + .replace("%s", "%02d" % seconds) + ) + elif display == "localized-no-seconds": days += weeks * 7 if days: if hours or minutes: - return npgettext('time format no seconds with day', '%d day %h:%m', '%d days %h:%m', days) \ - .replace('%d', str(days)).replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) - return ungettext('%d day', '%d days', days) % days + return ( + npgettext( + "time format no seconds with day", + "%d day %h:%m", + "%d days %h:%m", + days, + ) + .replace("%d", str(days)) + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + ) + return ungettext("%d day", "%d days", days) % days else: - return pgettext('hours and minutes', '%h:%m').replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) - elif display == 'concise': + return ( + pgettext("hours and minutes", "%h:%m") + .replace("%h", "%02d" % hours) + .replace("%m", "%02d" % minutes) + ) + elif display == "concise": days += weeks * 7 if days: - return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) + return "%dd %02d:%02d:%02d" % (days, hours, minutes, seconds) else: - return '%02d:%02d:%02d' % (hours, minutes, seconds) - elif display == 'noday': + return "%02d:%02d:%02d" % (hours, minutes, seconds) + elif display == "noday": days += weeks * 7 hours += days * 24 - return '%02d:%02d:%02d' % (hours, minutes, seconds) - elif display == 'minimal': - words = ['w', 'd', 'h', 'm', 's'] - elif display == 'short': - words = [' wks', ' days', ' hrs', ' min', ' sec'] + return "%02d:%02d:%02d" % (hours, minutes, seconds) + elif display == "minimal": + words = ["w", "d", "h", "m", "s"] + elif display == "short": + words = [" wks", " days", " hrs", " min", " sec"] else: - words = [' weeks', ' days', ' hours', ' minutes', ' seconds'] + words = [" weeks", " days", " hours", " minutes", " seconds"] values = [weeks, days, hours, minutes, seconds] for i in range(len(values)): if values[i]: if values[i] == 1 and len(words[i]) > 1: - result.append('%i%s' % (values[i], words[i].rstrip('s'))) + result.append("%i%s" % (values[i], words[i].rstrip("s"))) else: - result.append('%i%s' % (values[i], words[i])) + result.append("%i%s" % (values[i], words[i])) return sep.join(result) diff --git a/judge/utils/unicode.py b/judge/utils/unicode.py index 78cc176..b55a4b5 100644 --- a/judge/utils/unicode.py +++ b/judge/utils/unicode.py @@ -6,12 +6,12 @@ def utf8bytes(maybe_text): return if isinstance(maybe_text, six.binary_type): return maybe_text - return maybe_text.encode('utf-8') + return maybe_text.encode("utf-8") -def utf8text(maybe_bytes, errors='strict'): +def utf8text(maybe_bytes, errors="strict"): if maybe_bytes is None: return if isinstance(maybe_bytes, six.text_type): return maybe_bytes - return maybe_bytes.decode('utf-8', errors) + return maybe_bytes.decode("utf-8", errors) diff --git a/judge/utils/views.py b/judge/utils/views.py index 622f0e6..530caba 100644 --- a/judge/utils/views.py +++ b/judge/utils/views.py @@ -6,6 +6,7 @@ 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 on class based Views. @@ -22,34 +23,43 @@ def class_view_decorator(function_decorator): def generic_message(request, title, message, status=None): - return render(request, 'generic-message.html', { - 'message': message, - 'title': title, - }, status=status) + return render( + request, + "generic-message.html", + { + "message": message, + "title": title, + }, + status=status, + ) def paginate_query_context(request): query = request.GET.copy() - query.setlist('page', []) + query.setlist("page", []) query = query.urlencode() if query: - return {'page_prefix': '%s?%s&page=' % (request.path, query), - 'first_page_href': '%s?%s' % (request.path, query)} + return { + "page_prefix": "%s?%s&page=" % (request.path, query), + "first_page_href": "%s?%s" % (request.path, query), + } else: - return {'page_prefix': '%s?page=' % request.path, - 'first_page_href': request.path} + return { + "page_prefix": "%s?page=" % request.path, + "first_page_href": request.path, + } class TitleMixin(object): - title = '(untitled)' + title = "(untitled)" content_title = None def get_context_data(self, **kwargs): context = super(TitleMixin, self).get_context_data(**kwargs) - context['title'] = self.get_title() + context["title"] = self.get_title() content_title = self.get_content_title() if content_title is not None: - context['content_title'] = content_title + context["content_title"] = content_title return context def get_content_title(self): @@ -60,10 +70,18 @@ class TitleMixin(object): class DiggPaginatorMixin(object): - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, - orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) class QueryStringSortMixin(object): @@ -75,8 +93,11 @@ class QueryStringSortMixin(object): return self.default_sort def get(self, request, *args, **kwargs): - order = request.GET.get('order', '') - if not ((not order.startswith('-') or order.count('-') == 1) and (order.lstrip('-') in self.all_sorts)): + order = request.GET.get("order", "") + if not ( + (not order.startswith("-") or order.count("-") == 1) + and (order.lstrip("-") in self.all_sorts) + ): order = self.get_default_sort_order(request) self.order = order @@ -84,17 +105,26 @@ class QueryStringSortMixin(object): def get_sort_context(self): query = self.request.GET.copy() - query.setlist('order', []) + query.setlist("order", []) query = query.urlencode() - sort_prefix = '%s?%s&order=' % (self.request.path, query) if query else '%s?order=' % self.request.path - current = self.order.lstrip('-') + sort_prefix = ( + "%s?%s&order=" % (self.request.path, query) + if query + else "%s?order=" % self.request.path + ) + current = self.order.lstrip("-") - links = {key: sort_prefix + ('-' if key in self.default_desc else '') + key for key in self.all_sorts} - links[current] = sort_prefix + ('' if self.order.startswith('-') else '-') + current + links = { + key: sort_prefix + ("-" if key in self.default_desc else "") + key + for key in self.all_sorts + } + links[current] = ( + sort_prefix + ("" if self.order.startswith("-") else "-") + current + ) - order = {key: '' for key in self.all_sorts} - order[current] = ' \u25BE' if self.order.startswith('-') else u' \u25B4' - return {'sort_links': links, 'sort_order': order} + order = {key: "" for key in self.all_sorts} + order[current] = " \u25BE" if self.order.startswith("-") else " \u25B4" + return {"sort_links": links, "sort_order": order} def get_sort_paginate_context(self): return paginate_query_context(self.request) diff --git a/judge/views/__init__.py b/judge/views/__init__.py index 37d173d..94a01c5 100644 --- a/judge/views/__init__.py +++ b/judge/views/__init__.py @@ -5,6 +5,6 @@ class TitledTemplateView(TemplateView): title = None def get_context_data(self, **kwargs): - if 'title' not in kwargs and self.title is not None: - kwargs['title'] = self.title + if "title" not in kwargs and self.title is not None: + kwargs["title"] = self.title return super(TitledTemplateView, self).get_context_data(**kwargs) diff --git a/judge/views/about.py b/judge/views/about.py index d328066..e503724 100644 --- a/judge/views/about.py +++ b/judge/views/about.py @@ -3,12 +3,20 @@ from django.utils.translation import gettext as _ def about(request): - return render(request, 'about/about.html', { - 'title': _('About'), - }) + return render( + request, + "about/about.html", + { + "title": _("About"), + }, + ) def custom_checker_sample(request): - return render(request, 'about/custom-checker-sample.html', { - 'title': _('Custom Checker Sample'), - }) + return render( + request, + "about/custom-checker-sample.html", + { + "title": _("Custom Checker Sample"), + }, + ) diff --git a/judge/views/api/api_v1.py b/judge/views/api/api_v1.py index bad7486..80f18f0 100644 --- a/judge/views/api/api_v1.py +++ b/judge/views/api/api_v1.py @@ -5,27 +5,40 @@ from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404 from dmoj import settings -from judge.models import Contest, ContestParticipation, ContestTag, Problem, Profile, Submission +from judge.models import ( + Contest, + ContestParticipation, + ContestTag, + Problem, + Profile, + Submission, +) def sane_time_repr(delta): days = delta.days hours = delta.seconds / 3600 minutes = (delta.seconds % 3600) / 60 - return '%02d:%02d:%02d' % (days, hours, minutes) + return "%02d:%02d:%02d" % (days, hours, minutes) def api_v1_contest_list(request): 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, - 'start_time': c.start_time.isoformat(), - 'end_time': c.end_time.isoformat(), - 'time_limit': c.time_limit and sane_time_repr(c.time_limit), - 'labels': list(map(attrgetter('name'), c.tag_list)), - } for c in queryset}) + Prefetch("tags", queryset=ContestTag.objects.only("name"), to_attr="tag_list") + ) + + return JsonResponse( + { + c.key: { + "name": c.name, + "start_time": c.start_time.isoformat(), + "end_time": c.end_time.isoformat(), + "time_limit": c.time_limit and sane_time_repr(c.time_limit), + "labels": list(map(attrgetter("name"), c.tag_list)), + } + for c in queryset + } + ) def api_v1_contest_detail(request, contest): @@ -33,61 +46,87 @@ def api_v1_contest_detail(request, contest): in_contest = contest.is_in_contest(request.user) 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) - .prefetch_related('user__organizations') - .annotate(username=F('user__user__username')) - .order_by('-score', 'cumtime') if can_see_rankings else []) + problems = list( + contest.contest_problems.select_related("problem") + .defer("problem__description") + .order_by("order") + ) + 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 [] + ) - can_see_problems = (in_contest or contest.ended or contest.is_editable_by(request.user)) + can_see_problems = ( + in_contest or contest.ended or contest.is_editable_by(request.user) + ) - return JsonResponse({ - 'time_limit': contest.time_limit and contest.time_limit.total_seconds(), - 'start_time': contest.start_time.isoformat(), - 'end_time': contest.end_time.isoformat(), - 'tags': list(contest.tags.values_list('name', flat=True)), - 'is_rated': contest.is_rated, - 'rate_all': contest.is_rated and contest.rate_all, - 'has_rating': contest.ratings.exists(), - 'rating_floor': contest.rating_floor, - 'rating_ceiling': contest.rating_ceiling, - 'format': { - 'name': contest.format_name, - 'config': contest.format_config, - }, - 'problems': [ - { - 'points': int(problem.points), - 'partial': problem.partial, - 'name': problem.problem.name, - 'code': problem.problem.code, - } for problem in problems] if can_see_problems else [], - 'rankings': [ - { - 'user': participation.username, - 'points': participation.score, - 'cumtime': participation.cumtime, - 'is_disqualified': participation.is_disqualified, - 'solutions': contest.format.get_problem_breakdown(participation, problems), - } for participation in participations], - }) + return JsonResponse( + { + "time_limit": contest.time_limit and contest.time_limit.total_seconds(), + "start_time": contest.start_time.isoformat(), + "end_time": contest.end_time.isoformat(), + "tags": list(contest.tags.values_list("name", flat=True)), + "is_rated": contest.is_rated, + "rate_all": contest.is_rated and contest.rate_all, + "has_rating": contest.ratings.exists(), + "rating_floor": contest.rating_floor, + "rating_ceiling": contest.rating_ceiling, + "format": { + "name": contest.format_name, + "config": contest.format_config, + }, + "problems": [ + { + "points": int(problem.points), + "partial": problem.partial, + "name": problem.problem.name, + "code": problem.problem.code, + } + for problem in problems + ] + if can_see_problems + else [], + "rankings": [ + { + "user": participation.username, + "points": participation.score, + "cumtime": participation.cumtime, + "is_disqualified": participation.is_disqualified, + "solutions": contest.format.get_problem_breakdown( + participation, problems + ), + } + for participation in participations + ], + } + ) def api_v1_problem_list(request): queryset = Problem.objects.filter(is_public=True, is_organization_private=False) - if settings.ENABLE_FTS and 'search' in request.GET: - query = ' '.join(request.GET.getlist('search')).strip() + if settings.ENABLE_FTS and "search" in request.GET: + query = " ".join(request.GET.getlist("search")).strip() if query: queryset = queryset.search(query) - queryset = queryset.values_list('code', 'points', 'partial', 'name', 'group__full_name') + queryset = queryset.values_list( + "code", "points", "partial", "name", "group__full_name" + ) - return JsonResponse({code: { - 'points': points, - 'partial': partial, - 'name': name, - 'group': group, - } for code, points, partial, name, group in queryset}) + return JsonResponse( + { + code: { + "points": points, + "partial": partial, + "name": name, + "group": group, + } + for code, points, partial, name, group in queryset + } + ) def api_v1_problem_info(request, problem): @@ -95,60 +134,83 @@ def api_v1_problem_info(request, problem): if not p.is_accessible_by(request.user): raise Http404() - return JsonResponse({ - 'name': p.name, - 'authors': list(p.authors.values_list('user__username', flat=True)), - 'types': list(p.types.values_list('full_name', flat=True)), - 'group': p.group.full_name, - 'time_limit': p.time_limit, - 'memory_limit': p.memory_limit, - 'points': p.points, - 'partial': p.partial, - 'languages': list(p.allowed_languages.values_list('key', flat=True)), - }) + return JsonResponse( + { + "name": p.name, + "authors": list(p.authors.values_list("user__username", flat=True)), + "types": list(p.types.values_list("full_name", flat=True)), + "group": p.group.full_name, + "time_limit": p.time_limit, + "memory_limit": p.memory_limit, + "points": p.points, + "partial": p.partial, + "languages": list(p.allowed_languages.values_list("key", flat=True)), + } + ) def api_v1_user_list(request): - queryset = Profile.objects.filter(is_unlisted=False).values_list('user__username', 'points', 'performance_points', - 'display_rank') - return JsonResponse({username: { - 'points': points, - 'performance_points': performance_points, - 'rank': rank, - } for username, points, performance_points, rank in queryset}) + queryset = Profile.objects.filter(is_unlisted=False).values_list( + "user__username", "points", "performance_points", "display_rank" + ) + return JsonResponse( + { + username: { + "points": points, + "performance_points": performance_points, + "rank": rank, + } + for username, points, performance_points, rank in queryset + } + ) def api_v1_user_info(request, user): profile = get_object_or_404(Profile, user__username=user) - submissions = list(Submission.objects.filter(case_points=F('case_total'), user=profile, problem__is_public=True, - problem__is_organization_private=False) - .values('problem').distinct().values_list('problem__code', flat=True)) + submissions = list( + Submission.objects.filter( + case_points=F("case_total"), + user=profile, + problem__is_public=True, + problem__is_organization_private=False, + ) + .values("problem") + .distinct() + .values_list("problem__code", flat=True) + ) resp = { - 'points': profile.points, - 'performance_points': profile.performance_points, - 'rank': profile.display_rank, - 'solved_problems': submissions, - 'organizations': list(profile.organizations.values_list('id', flat=True)), + "points": profile.points, + "performance_points": profile.performance_points, + "rank": profile.display_rank, + "solved_problems": submissions, + "organizations": list(profile.organizations.values_list("id", flat=True)), } last_rating = profile.ratings.last() contest_history = {} - participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True, - contest__is_private=False, - contest__is_organization_private=False) + 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__key", + "rating__rating", + "rating__mean", + "rating__performance", ): contest_history[contest_key] = { - 'rating': rating, - 'raw_rating': mean, - 'performance': performance, + "rating": rating, + "raw_rating": mean, + "performance": performance, } - resp['contests'] = { - 'current_rating': last_rating.rating if last_rating else None, - 'history': contest_history, + resp["contests"] = { + "current_rating": last_rating.rating if last_rating else None, + "history": contest_history, } return JsonResponse(resp) @@ -156,14 +218,30 @@ def api_v1_user_info(request, user): def api_v1_user_submissions(request, user): profile = get_object_or_404(Profile, user__username=user) - subs = Submission.objects.filter(user=profile, problem__is_public=True, problem__is_organization_private=False) + subs = Submission.objects.filter( + user=profile, problem__is_public=True, problem__is_organization_private=False + ) - return JsonResponse({sub['id']: { - 'problem': sub['problem__code'], - 'time': sub['time'], - 'memory': sub['memory'], - 'points': sub['points'], - 'language': sub['language__key'], - 'status': sub['status'], - 'result': sub['result'], - } for sub in subs.values('id', 'problem__code', 'time', 'memory', 'points', 'language__key', 'status', 'result')}) + return JsonResponse( + { + sub["id"]: { + "problem": sub["problem__code"], + "time": sub["time"], + "memory": sub["memory"], + "points": sub["points"], + "language": sub["language__key"], + "status": sub["status"], + "result": sub["result"], + } + for sub in subs.values( + "id", + "problem__code", + "time", + "memory", + "points", + "language__key", + "status", + "result", + ) + } + ) diff --git a/judge/views/api/api_v2.py b/judge/views/api/api_v2.py index 8e512b3..a750d98 100644 --- a/judge/views/api/api_v2.py +++ b/judge/views/api/api_v2.py @@ -9,7 +9,7 @@ from judge.views.contests import contest_ranking_list def error(message): - return JsonResponse({'error': message}, status=422) + return JsonResponse({"error": message}, status=422) def api_v2_user_info(request): @@ -44,9 +44,9 @@ def api_v2_user_info(request): // ... ] } - """ + """ try: - username = request.GET['username'] + username = request.GET["username"] except KeyError: return error("no username passed") if not username: @@ -56,66 +56,90 @@ def api_v2_user_info(request): except Profile.DoesNotExist: return error("no such user") - last_rating = list(profile.ratings.order_by('-contest__end_time')) + last_rating = list(profile.ratings.order_by("-contest__end_time")) resp = { "rank": profile.display_rank, - "organizations": list(profile.organizations.values_list('key', flat=True)), + "organizations": list(profile.organizations.values_list("key", flat=True)), } contest_history = [] - for participation in (ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True) - .order_by('-contest__end_time')): + for participation in ContestParticipation.objects.filter( + user=profile, virtual=0, contest__is_visible=True + ).order_by("-contest__end_time"): contest = participation.contest - problems = list(contest.contest_problems.select_related('problem').defer('problem__description') - .order_by('order')) - rank, result = next(filter(lambda data: data[1].user == profile.user, - ranker(contest_ranking_list(contest, problems), - key=attrgetter('points', 'cumtime')))) + problems = list( + contest.contest_problems.select_related("problem") + .defer("problem__description") + .order_by("order") + ) + rank, result = next( + filter( + lambda data: data[1].user == profile.user, + ranker( + contest_ranking_list(contest, problems), + key=attrgetter("points", "cumtime"), + ), + ) + ) - contest_history.append({ - 'contest': { - 'code': contest.key, - 'name': contest.name, - 'tags': list(contest.tags.values_list('name', flat=True)), - 'time_limit': contest.time_limit and contest.time_limit.total_seconds(), - 'start_time': contest.start_time.isoformat(), - 'end_time': contest.end_time.isoformat(), - }, - 'rank': rank, - 'rating': result.participation_rating, - }) + contest_history.append( + { + "contest": { + "code": contest.key, + "name": contest.name, + "tags": list(contest.tags.values_list("name", flat=True)), + "time_limit": contest.time_limit + and contest.time_limit.total_seconds(), + "start_time": contest.start_time.isoformat(), + "end_time": contest.end_time.isoformat(), + }, + "rank": rank, + "rating": result.participation_rating, + } + ) - resp['contests'] = { + resp["contests"] = { "current_rating": last_rating[0].rating if last_rating else None, - 'history': contest_history, + "history": contest_history, } solved_problems = [] attempted_problems = [] - problem_data = (Submission.objects.filter(points__gt=0, user=profile, problem__is_public=True, - problem__is_organization_private=False) - .annotate(max_pts=Max('points')) - .values_list('max_pts', 'problem__points', 'problem__code') - .distinct()) + problem_data = ( + Submission.objects.filter( + points__gt=0, + user=profile, + problem__is_public=True, + problem__is_organization_private=False, + ) + .annotate(max_pts=Max("points")) + .values_list("max_pts", "problem__points", "problem__code") + .distinct() + ) for awarded_pts, max_pts, problem in problem_data: if awarded_pts == max_pts: solved_problems.append(problem) else: - attempted_problems.append({ - 'awarded': awarded_pts, - 'max': max_pts, - 'problem': problem, - }) + attempted_problems.append( + { + "awarded": awarded_pts, + "max": max_pts, + "problem": problem, + } + ) - resp['problems'] = { - 'points': profile.points, - 'solved': solved_problems, - 'attempted': attempted_problems, - 'authored': list(Problem.objects.filter(is_public=True, is_organization_private=False, authors=profile) - .values_list('code', flat=True)), + resp["problems"] = { + "points": profile.points, + "solved": solved_problems, + "attempted": attempted_problems, + "authored": list( + Problem.objects.filter( + is_public=True, is_organization_private=False, authors=profile + ).values_list("code", flat=True) + ), } return JsonResponse(resp) diff --git a/judge/views/blog.py b/judge/views/blog.py index 828f54f..9ce0fdc 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -8,8 +8,17 @@ from django.utils.translation import ugettext as _ from django.views.generic import ListView from judge.comments import CommentedDetailView -from judge.models import BlogPost, Comment, Contest, Language, Problem, ProblemClarification, Profile, Submission, \ - Ticket +from judge.models import ( + BlogPost, + Comment, + Contest, + Language, + Problem, + ProblemClarification, + Profile, + Submission, + Ticket, +) from judge.utils.cachedict import CacheDict from judge.utils.diggpaginator import DiggPaginator from judge.utils.problems import user_completed_ids @@ -19,32 +28,44 @@ from judge.utils.views import TitleMixin # General view for all content list on home feed class FeedView(ListView): - template_name = 'blog/list.html' + template_name = "blog/list.html" title = None - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, - orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) def get_context_data(self, **kwargs): context = super(FeedView, self).get_context_data(**kwargs) - context['has_clarifications'] = False + 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') + 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["can_edit_contest"] = True - context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) + context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page)) - 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) - context['language_count'] = lazy(Language.objects.count, int, int) + context["user_count"] = lazy(Profile.objects.count, int, int) + context["problem_count"] = lazy( + Problem.objects.filter(is_public=True).count, int, int + ) + context["submission_count"] = lazy(Submission.objects.count, int, int) + context["language_count"] = lazy(Language.objects.count, int, int) now = timezone.now() @@ -57,31 +78,44 @@ class FeedView(ListView): # .annotate(points=Max('points'), latest=Max('date')) # .order_by('-latest') # [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) - - 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) + .order_by("start_time") + ) - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) + context["current_contests"] = visible_contests.filter( + start_time__lte=now, end_time__gt=now + ) + context["future_contests"] = visible_contests.filter(start_time__gt=now) - context['top_rated'] = Profile.objects.filter(is_unlisted=False).order_by('-rating')[:10] - context['top_scorer'] = Profile.objects.filter(is_unlisted=False).order_by('-performance_points')[:10] + visible_contests = Contest.get_visible_contests(self.request.user).filter( + is_visible=True + ) + + context["top_rated"] = Profile.objects.filter(is_unlisted=False).order_by( + "-rating" + )[:10] + context["top_scorer"] = Profile.objects.filter(is_unlisted=False).order_by( + "-performance_points" + )[:10] return context - + class PostList(FeedView): model = BlogPost paginate_by = 10 - context_object_name = 'posts' + context_object_name = "posts" def get_queryset(self): - queryset = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) \ - .order_by('-sticky', '-publish_on') \ - .prefetch_related('authors__user', 'organizations') - if not self.request.user.has_perm('judge.edit_all_post'): + queryset = ( + BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) + .order_by("-sticky", "-publish_on") + .prefetch_related("authors__user", "organizations") + ) + if not self.request.user.has_perm("judge.edit_all_post"): filter = Q(is_organization_private=False) if self.request.user.is_authenticated: filter |= Q(organizations__in=self.request.profile.organizations.all()) @@ -90,15 +124,20 @@ class PostList(FeedView): def get_context_data(self, **kwargs): context = super(PostList, self).get_context_data(**kwargs) - context['title'] = self.title or _('Page %d of Posts') % context['page_obj'].number - context['first_page_href'] = reverse('home') - context['page_prefix'] = reverse('blog_post_list') - context['page_type'] = 'blog' - context['post_comment_counts'] = { - int(page[2:]): count for page, count in - 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["title"] = ( + self.title or _("Page %d of Posts") % context["page_obj"].number + ) + context["first_page_href"] = reverse("home") + context["page_prefix"] = reverse("blog_post_list") + context["page_type"] = "blog" + context["post_comment_counts"] = { + int(page[2:]): count + for page, count in 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() } return context @@ -106,39 +145,49 @@ class PostList(FeedView): class TicketFeed(FeedView): model = Ticket - context_object_name = 'tickets' + context_object_name = "tickets" paginate_by = 30 def get_queryset(self, is_own=True): profile = self.request.profile if is_own: if self.request.user.is_authenticated: - return (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') - .prefetch_related('linked_item').select_related('user__user')) + return ( + Ticket.objects.filter( + Q(user=profile) | Q(assignees__in=[profile]), is_open=True + ) + .order_by("-id") + .prefetch_related("linked_item") + .select_related("user__user") + ) else: return [] else: # Superusers better be staffs, not the spell-casting kind either. if self.request.user.is_staff: - tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item') - .select_related('user__user')) + tickets = ( + Ticket.objects.order_by("-id") + .filter(is_open=True) + .prefetch_related("linked_item") + .select_related("user__user") + ) return filter_visible_tickets(tickets, self.request.user, profile) else: return [] def get_context_data(self, **kwargs): context = super(TicketFeed, self).get_context_data(**kwargs) - context['page_type'] = 'ticket' - context['first_page_href'] = self.request.path - context['page_prefix'] = '?page=' - context['title'] = _('Ticket feed') + context["page_type"] = "ticket" + context["first_page_href"] = self.request.path + context["page_prefix"] = "?page=" + context["title"] = _("Ticket feed") return context class CommentFeed(FeedView): model = Comment - context_object_name = 'comments' + context_object_name = "comments" paginate_by = 50 def get_queryset(self): @@ -146,29 +195,29 @@ class CommentFeed(FeedView): def get_context_data(self, **kwargs): context = super(CommentFeed, self).get_context_data(**kwargs) - context['page_type'] = 'comment' - context['first_page_href'] = self.request.path - context['page_prefix'] = '?page=' - context['title'] = _('Comment feed') + context["page_type"] = "comment" + context["first_page_href"] = self.request.path + context["page_prefix"] = "?page=" + context["title"] = _("Comment feed") return context class PostView(TitleMixin, CommentedDetailView): model = BlogPost - pk_url_kwarg = 'id' - context_object_name = 'post' - template_name = 'blog/blog.html' + pk_url_kwarg = "id" + context_object_name = "post" + template_name = "blog/blog.html" def get_title(self): return self.object.title def get_comment_page(self): - return 'b:%s' % self.object.id + return "b:%s" % self.object.id def get_context_data(self, **kwargs): context = super(PostView, self).get_context_data(**kwargs) - context['og_image'] = self.object.og_image + context["og_image"] = self.object.og_image return context def get_object(self, queryset=None): diff --git a/judge/views/comment.py b/judge/views/comment.py index 7550d97..edba7f3 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -4,7 +4,12 @@ from django.core.exceptions import PermissionDenied from django.db import IntegrityError, transaction from django.db.models import F from django.forms.models import ModelForm -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseForbidden, +) from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST @@ -18,27 +23,41 @@ from judge.utils.views import TitleMixin from judge.widgets import MathJaxPagedownWidget from judge.comments import add_mention_notifications, del_mention_notifications -__all__ = ['upvote_comment', 'downvote_comment', 'CommentEditAjax', 'CommentContent', - 'CommentEdit'] +__all__ = [ + "upvote_comment", + "downvote_comment", + "CommentEditAjax", + "CommentContent", + "CommentEdit", +] @login_required def vote_comment(request, delta): if abs(delta) != 1: - return HttpResponseBadRequest(_('Messing around, are we?'), content_type='text/plain') + return HttpResponseBadRequest( + _("Messing around, are we?"), content_type="text/plain" + ) - if request.method != 'POST': + if request.method != "POST": return HttpResponseForbidden() - if 'id' not in request.POST: + if "id" not in request.POST: return HttpResponseBadRequest() - if not request.user.is_staff and not request.profile.submission_set.filter(points=F('problem__points')).exists(): - return HttpResponseBadRequest(_('You must solve at least one problem before you can vote.'), - content_type='text/plain') + if ( + not request.user.is_staff + and not request.profile.submission_set.filter( + points=F("problem__points") + ).exists() + ): + return HttpResponseBadRequest( + _("You must solve at least one problem before you can vote."), + content_type="text/plain", + ) try: - comment_id = int(request.POST['id']) + comment_id = int(request.POST["id"]) except ValueError: return HttpResponseBadRequest() else: @@ -56,18 +75,22 @@ def vote_comment(request, delta): except IntegrityError: with LockModel(write=(CommentVote,)): try: - vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile) + vote = CommentVote.objects.get( + comment_id=comment_id, voter=request.profile + ) except CommentVote.DoesNotExist: # We must continue racing in case this is exploited to manipulate votes. continue if -vote.score != delta: - return HttpResponseBadRequest(_('You already voted.'), content_type='text/plain') + return HttpResponseBadRequest( + _("You already voted."), content_type="text/plain" + ) vote.delete() - Comment.objects.filter(id=comment_id).update(score=F('score') - vote.score) + Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score) else: - Comment.objects.filter(id=comment_id).update(score=F('score') + delta) + Comment.objects.filter(id=comment_id).update(score=F("score") + delta) break - return HttpResponse('success', content_type='text/plain') + return HttpResponse("success", content_type="text/plain") def upvote_comment(request): @@ -80,26 +103,28 @@ def downvote_comment(request): class CommentMixin(object): model = Comment - pk_url_kwarg = 'id' - context_object_name = 'comment' + pk_url_kwarg = "id" + context_object_name = "comment" class CommentRevisionAjax(CommentMixin, DetailView): - template_name = 'comments/revision-ajax.html' + template_name = "comments/revision-ajax.html" def get_context_data(self, **kwargs): context = super(CommentRevisionAjax, self).get_context_data(**kwargs) - revisions = Version.objects.get_for_object(self.object).order_by('-revision') + revisions = Version.objects.get_for_object(self.object).order_by("-revision") try: - wanted = min(max(int(self.request.GET.get('revision', 0)), 0), len(revisions) - 1) + wanted = min( + max(int(self.request.GET.get("revision", 0)), 0), len(revisions) - 1 + ) except ValueError: raise Http404 - context['revision'] = revisions[wanted] + context["revision"] = revisions[wanted] return context def get_object(self, queryset=None): comment = super(CommentRevisionAjax, self).get_object(queryset) - if comment.hidden and not self.request.user.has_perm('judge.change_comment'): + if comment.hidden and not self.request.user.has_perm("judge.change_comment"): raise Http404() return comment @@ -107,13 +132,15 @@ class CommentRevisionAjax(CommentMixin, DetailView): class CommentEditForm(ModelForm): class Meta: model = Comment - fields = ['body'] + fields = ["body"] if MathJaxPagedownWidget is not None: - widgets = {'body': MathJaxPagedownWidget(attrs={'id': 'id-edit-comment-body'})} + widgets = { + "body": MathJaxPagedownWidget(attrs={"id": "id-edit-comment-body"}) + } class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): - template_name = 'comments/edit-ajax.html' + template_name = "comments/edit-ajax.html" form_class = CommentEditForm def form_valid(self, form): @@ -123,7 +150,7 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): add_mention_notifications(comment) with transaction.atomic(), revisions.create_revision(): - revisions.set_comment(_('Edited from site')) + revisions.set_comment(_("Edited from site")) revisions.set_user(self.request.user) return super(CommentEditAjax, self).form_valid(form) @@ -132,7 +159,7 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): def get_object(self, queryset=None): comment = super(CommentEditAjax, self).get_object(queryset) - if self.request.user.has_perm('judge.change_comment'): + if self.request.user.has_perm("judge.change_comment"): return comment profile = self.request.profile if profile != comment.author or profile.mute or comment.hidden: @@ -141,36 +168,37 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): class CommentEdit(TitleMixin, CommentEditAjax): - template_name = 'comments/edit.html' + template_name = "comments/edit.html" def get_title(self): - return _('Editing comment') + return _("Editing comment") class CommentContent(CommentMixin, DetailView): - template_name = 'comments/content.html' + template_name = "comments/content.html" class CommentVotesAjax(PermissionRequiredMixin, CommentMixin, DetailView): - template_name = 'comments/votes.html' - permission_required = 'judge.change_commentvote' + template_name = "comments/votes.html" + permission_required = "judge.change_commentvote" def get_context_data(self, **kwargs): context = super(CommentVotesAjax, self).get_context_data(**kwargs) - context['votes'] = (self.object.votes.select_related('voter__user') - .only('id', 'voter__display_rank', 'voter__user__username', 'score')) + context["votes"] = self.object.votes.select_related("voter__user").only( + "id", "voter__display_rank", "voter__user__username", "score" + ) return context @require_POST def comment_hide(request): - if not request.user.has_perm('judge.change_comment'): + if not request.user.has_perm("judge.change_comment"): raise PermissionDenied() try: - comment_id = int(request.POST['id']) + comment_id = int(request.POST["id"]) except ValueError: return HttpResponseBadRequest() comment = get_object_or_404(Comment, id=comment_id) comment.get_descendants(include_self=True).update(hidden=True) - return HttpResponse('ok') + return HttpResponse("ok") diff --git a/judge/views/contests.py b/judge/views/contests.py index 8dcd43b..f16fdae 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -13,9 +13,28 @@ 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, F, 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, JsonResponse, HttpResponseNotAllowed +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, + JsonResponse, + HttpResponseNotAllowed, +) from django.shortcuts import get_object_or_404, render from django.template.defaultfilters import date as date_filter from django.urls import reverse, reverse_lazy @@ -26,27 +45,62 @@ from django.utils.safestring import mark_safe from django.utils.timezone import make_aware from django.utils.translation import gettext as _, gettext_lazy from django.views.generic import ListView, TemplateView -from django.views.generic.detail import BaseDetailView, DetailView, SingleObjectMixin, View +from django.views.generic.detail import ( + BaseDetailView, + DetailView, + SingleObjectMixin, + View, +) 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, ProblemClarification +from judge.models import ( + Contest, + ContestMoss, + ContestParticipation, + ContestProblem, + ContestTag, + 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, get_histogram -from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \ - generic_message +from judge.utils.views import ( + DiggPaginatorMixin, + QueryStringSortMixin, + SingleObjectFormView, + TitleMixin, + generic_message, +) from judge.widgets import HeavyPreviewPageDownWidget -__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar', - 'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax', - 'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list', - 'base_contest_ranking_list', 'ContestClarificationView', 'update_contest_mode'] +__all__ = [ + "ContestList", + "ContestDetail", + "ContestRanking", + "ContestJoin", + "ContestLeave", + "ContestCalendar", + "ContestClone", + "ContestStats", + "ContestMossView", + "ContestMossDelete", + "contest_ranking_ajax", + "ContestParticipationList", + "ContestParticipationDisqualify", + "get_contest_ranking_list", + "base_contest_ranking_list", + "ContestClarificationView", + "update_contest_mode", +] def _find_contest(request, key, private_check=True): @@ -55,8 +109,15 @@ def _find_contest(request, key, private_check=True): if private_check and not contest.is_accessible_by(request.user): raise ObjectDoesNotExist() except ObjectDoesNotExist: - return generic_message(request, _('No such contest'), - _('Could not find a contest with the key "%s".') % key, status=404), False + return ( + generic_message( + request, + _("No such contest"), + _('Could not find a contest with the key "%s".') % key, + status=404, + ), + False, + ) return contest, True @@ -65,16 +126,18 @@ class ContestListMixin(object): return Contest.get_visible_contests(self.request.user) -class ContestList(QueryStringSortMixin, 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' - all_sorts = frozenset(('name', 'user_count', 'start_time')) - default_desc = frozenset(('name', 'user_count')) - default_sort = '-start_time' - + template_name = "contest/list.html" + title = gettext_lazy("Contests") + context_object_name = "past_contests" + all_sorts = frozenset(("name", "user_count", "start_time")) + default_desc = frozenset(("name", "user_count")) + default_sort = "-start_time" + @cached_property def _now(self): return timezone.now() @@ -83,30 +146,40 @@ class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestL self.contest_query = None self.org_query = [] - if 'orgs' in self.request.GET: + if "orgs" in self.request.GET: try: - self.org_query = list(map(int, request.GET.getlist('orgs'))) + self.org_query = list(map(int, request.GET.getlist("orgs"))) except ValueError: pass return super(ContestList, self).get(request, *args, **kwargs) def _get_queryset(self): - queryset = super(ContestList, self).get_queryset() \ - .prefetch_related('tags', 'organizations', 'authors', 'curators', 'testers') - - if 'contest' in self.request.GET: - self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip() + queryset = ( + super(ContestList, self) + .get_queryset() + .prefetch_related("tags", "organizations", "authors", "curators", "testers") + ) + + if "contest" in self.request.GET: + self.contest_query = query = " ".join( + self.request.GET.getlist("contest") + ).strip() if query: queryset = queryset.filter( - Q(key__icontains=query) | Q(name__icontains=query)) + Q(key__icontains=query) | Q(name__icontains=query) + ) if self.org_query: queryset = queryset.filter(organizations__in=self.org_query) return queryset def get_queryset(self): - return self._get_queryset().order_by(self.order, 'key').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) @@ -118,26 +191,31 @@ class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestL present.append(contest) 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__authors', 'contest__curators', 'contest__testers')\ - .annotate(key=F('contest__key')): + for participation in ( + ContestParticipation.objects.filter( + virtual=0, user=self.request.profile, contest_id__in=present + ) + .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', 'key')) - present.sort(key=attrgetter('end_time', 'key')) - future.sort(key=attrgetter('start_time')) - context['active_participations'] = active - context['current_contests'] = present - context['future_contests'] = future - context['now'] = self._now - context['first_page_href'] = '.' - context['contest_query'] = self.contest_query - context['org_query'] = self.org_query - context['organizations'] = Organization.objects.all() + 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 + context["future_contests"] = future + context["now"] = self._now + context["first_page_href"] = "." + context["contest_query"] = self.contest_query + context["org_query"] = self.org_query + context["organizations"] = Organization.objects.all() context.update(self.get_sort_context()) context.update(self.get_sort_paginate_context()) return context @@ -152,10 +230,10 @@ class PrivateContestError(Exception): class ContestMixin(object): - context_object_name = 'contest' + context_object_name = "contest" model = Contest - slug_field = 'key' - slug_url_kwarg = 'contest' + slug_field = "key" + slug_url_kwarg = "contest" @cached_property def is_editor(self): @@ -168,7 +246,7 @@ class ContestMixin(object): 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) @@ -177,35 +255,43 @@ class ContestMixin(object): context = super(ContestMixin, self).get_context_data(**kwargs) if self.request.user.is_authenticated: try: - context['live_participation'] = ( - self.request.profile.contest_history.get( - contest=self.object, - virtual=ContestParticipation.LIVE, - ) + 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 + context["live_participation"] = None + context["has_joined"] = False else: - context['has_joined'] = True + context["has_joined"] = True else: - context['live_participation'] = None - context['has_joined'] = False - - context['now'] = timezone.now() - context['is_editor'] = self.is_editor - context['is_tester'] = self.is_tester - context['can_edit'] = self.can_edit + context["live_participation"] = None + context["has_joined"] = False + + context["now"] = timezone.now() + 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, - self.object.description, 'contest') - context['meta_description'] = self.object.summary or metadata[0] - context['og_image'] = self.object.og_image or metadata[1] - context['has_moss_api_key'] = settings.MOSS_API_KEY is not None - context['logo_override_image'] = self.object.logo_override_image - if not context['logo_override_image'] and self.object.organizations.count() == 1: - context['logo_override_image'] = self.object.organizations.first().logo_override_image + metadata = generate_opengraph( + "generated-meta-contest:%d" % self.object.id, + self.object.description, + "contest", + ) + context["meta_description"] = self.object.summary or metadata[0] + context["og_image"] = self.object.og_image or metadata[1] + context["has_moss_api_key"] = settings.MOSS_API_KEY is not None + context["logo_override_image"] = self.object.logo_override_image + if ( + not context["logo_override_image"] + and self.object.organizations.count() == 1 + ): + context[ + "logo_override_image" + ] = self.object.organizations.first().logo_override_image return context @@ -213,30 +299,48 @@ class ContestMixin(object): contest = super(ContestMixin, self).get_object(queryset) profile = self.request.profile - if (profile is not None and - ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()): + if ( + profile is not None + and ContestParticipation.objects.filter( + id=profile.current_contest_id, contest_id=contest.id + ).exists() + ): return contest try: contest.access_check(self.request.user) except Contest.PrivateContest: - raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private, - contest.organizations.all()) + 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, - contest.is_organization_private, contest.organizations.all()) + private_contest_error = PrivateContestError( + contest.name, + contest.is_private, + contest.is_organization_private, + contest.organizations.all(), + ) if profile is None: raise private_contest_error - if user.has_perm('judge.edit_all_contest'): + if user.has_perm("judge.edit_all_contest"): return contest - if not (contest.is_organization_private and - contest.organizations.filter(id__in=profile.organizations.all()).exists()) and \ - not (contest.is_private and contest.private_contestants.filter(id=profile.id).exists()): + if not ( + contest.is_organization_private + and contest.organizations.filter( + id__in=profile.organizations.all() + ).exists() + ) and not ( + contest.is_private + and contest.private_contestants.filter(id=profile.id).exists() + ): raise private_contest_error return contest @@ -247,41 +351,63 @@ class ContestMixin(object): except Http404: key = kwargs.get(self.slug_url_kwarg, None) if key: - return generic_message(request, _('No such contest'), - _('Could not find a contest with the key "%s".') % key) + return generic_message( + request, + _("No such contest"), + _('Could not find a contest with the key "%s".') % key, + ) else: - return generic_message(request, _('No such contest'), - _('Could not find such contest.')) + return generic_message( + request, _("No such contest"), _("Could not find such contest.") + ) except PrivateContestError as e: - return render(request, 'contest/private.html', { - 'error': e, 'title': _('Access to contest "%s" denied') % e.name, - }, status=403) + return render( + request, + "contest/private.html", + { + "error": e, + "title": _('Access to contest "%s" denied') % e.name, + }, + status=403, + ) class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView): - template_name = 'contest/contest.html' + template_name = "contest/contest.html" def get_comment_page(self): - return 'c:%s' % self.object.key + return "c:%s" % self.object.key def get_title(self): return self.object.name def get_context_data(self, **kwargs): context = super(ContestDetail, self).get_context_data(**kwargs) - context['contest_problems'] = Problem.objects.filter(contests__contest=self.object) \ - .order_by('contests__order').defer('description') \ - .annotate(has_public_editorial=Sum(Case(When(solution__is_public=True, then=1), - default=0, output_field=IntegerField()))) \ + context["contest_problems"] = ( + Problem.objects.filter(contests__contest=self.object) + .order_by("contests__order") + .defer("description") + .annotate( + has_public_editorial=Sum( + Case( + When(solution__is_public=True, then=1), + default=0, + output_field=IntegerField(), + ) + ) + ) .add_i18n_name(self.request.LANGUAGE_CODE) + ) return context -class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView): - title = _('Clone Contest') - template_name = 'contest/clone.html' +class ContestClone( + ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView +): + title = _("Clone Contest") + template_name = "contest/clone.html" form_class = ContestCloneForm - permission_required = 'judge.clone_contest' + permission_required = "judge.clone_contest" def form_valid(self, form): contest = self.object @@ -295,7 +421,7 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje contest.pk = None contest.is_visible = False contest.user_count = 0 - contest.key = form.cleaned_data['key'] + contest.key = form.cleaned_data["key"] contest.save() contest.tags.set(tags) @@ -309,7 +435,9 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje problem.pk = None ContestProblem.objects.bulk_create(contest_problems) - return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest.id,))) + return HttpResponseRedirect( + reverse("admin:judge_contest_change", args=(contest.id,)) + ) class ContestAccessDenied(Exception): @@ -321,7 +449,7 @@ class ContestAccessCodeForm(forms.Form): def __init__(self, *args, **kwargs): super(ContestAccessCodeForm, self).__init__(*args, **kwargs) - self.fields['access_code'].widget.attrs.update({'autocomplete': 'off'}) + self.fields["access_code"].widget.attrs.update({"autocomplete": "off"}) class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): @@ -334,7 +462,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): try: return self.join_contest(request) except ContestAccessDenied: - if request.POST.get('access_code'): + if request.POST.get("access_code"): return self.ask_for_access_code(ContestAccessCodeForm(request.POST)) else: return HttpResponseRedirect(request.path) @@ -343,30 +471,59 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): contest = self.object 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) + return generic_message( + request, + _("Contest not ongoing"), + _('"%s" is not currently ongoing.') % contest.name, + ) profile = request.profile if profile.current_contest is not None: - return generic_message(request, _('Already in contest'), - _('You are already in a contest: "%s".') % profile.current_contest.contest.name) + return generic_message( + request, + _("Already in contest"), + _('You are already in a contest: "%s".') + % profile.current_contest.contest.name, + ) - if not request.user.is_superuser and contest.banned_users.filter(id=profile.id).exists(): - return generic_message(request, _('Banned from joining'), - _('You have been declared persona non grata for this contest. ' - 'You are permanently barred from joining this contest.')) + if ( + not request.user.is_superuser + and contest.banned_users.filter(id=profile.id).exists() + ): + return generic_message( + request, + _("Banned from joining"), + _( + "You have been declared persona non grata for this contest. " + "You are permanently barred from joining this contest." + ), + ) - requires_access_code = (not self.can_edit 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() while True: - virtual_id = max((ContestParticipation.objects.filter(contest=contest, user=profile) - .aggregate(virtual_id=Max('virtual'))['virtual_id'] or 0) + 1, 1) + virtual_id = max( + ( + ContestParticipation.objects.filter( + contest=contest, user=profile + ).aggregate(virtual_id=Max("virtual"))["virtual_id"] + or 0 + ) + + 1, + 1, + ) try: participation = ContestParticipation.objects.create( - contest=contest, user=profile, virtual=virtual_id, + contest=contest, + user=profile, + virtual=virtual_id, real_start=timezone.now(), ) # There is obviously a race condition here, so we keep trying until we win the race. @@ -379,43 +536,56 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView): LIVE = ContestParticipation.LIVE try: participation = ContestParticipation.objects.get( - contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE), + 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=(SPECTATE if self.is_editor or self.is_tester else LIVE), + 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=SPECTATE, - defaults={'real_start': timezone.now()}, + contest=contest, + user=profile, + virtual=SPECTATE, + defaults={"real_start": timezone.now()}, )[0] profile.current_contest = participation profile.save() contest._updating_stats_only = True contest.update_user_count() - return HttpResponseRedirect(reverse('problem_list')) + return HttpResponseRedirect(reverse("problem_list")) def ask_for_access_code(self, form=None): contest = self.object wrong_code = False if form: if form.is_valid(): - if form.cleaned_data['access_code'] == contest.access_code: - return self.join_contest(self.request, form.cleaned_data['access_code']) + if form.cleaned_data["access_code"] == contest.access_code: + return self.join_contest( + self.request, form.cleaned_data["access_code"] + ) wrong_code = True else: form = ContestAccessCodeForm() - return render(self.request, 'contest/access_code.html', { - 'form': form, 'wrong_code': wrong_code, - 'title': _('Enter access code for "%s"') % contest.name, - }) + return render( + self.request, + "contest/access_code.html", + { + "form": form, + "wrong_code": wrong_code, + "title": _('Enter access code for "%s"') % contest.name, + }, + ) class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView): @@ -423,29 +593,38 @@ class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView): contest = self.get_object() profile = request.profile - if profile.current_contest is None or profile.current_contest.contest_id != contest.id: - return generic_message(request, _('No such contest'), - _('You are not in contest "%s".') % contest.key, 404) + if ( + profile.current_contest is None + or profile.current_contest.contest_id != contest.id + ): + return generic_message( + request, + _("No such contest"), + _('You are not in contest "%s".') % contest.key, + 404, + ) profile.remove_contest() - request.session['contest_mode'] = True # reset contest_mode - return HttpResponseRedirect(reverse('contest_view', args=(contest.key,))) + request.session["contest_mode"] = True # reset contest_mode + return HttpResponseRedirect(reverse("contest_view", args=(contest.key,))) -ContestDay = namedtuple('ContestDay', 'date weekday is_pad is_today starts ends oneday') +ContestDay = namedtuple("ContestDay", "date weekday is_pad is_today starts ends oneday") class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): firstweekday = SUNDAY - weekday_classes = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] - template_name = 'contest/calendar.html' + weekday_classes = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] + template_name = "contest/calendar.html" def get(self, request, *args, **kwargs): try: - self.year = int(kwargs['year']) - self.month = int(kwargs['month']) + self.year = int(kwargs["year"]) + self.month = int(kwargs["month"]) except (KeyError, ValueError): - raise ImproperlyConfigured(_('ContestCalendar requires integer year and month')) + raise ImproperlyConfigured( + _("ContestCalendar requires integer year and month") + ) self.today = timezone.now().date() return self.render() @@ -455,12 +634,16 @@ 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)) + contests = self.get_queryset().filter( + Q(start_time__gte=start, start_time__lt=end) + | 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() - end_date = timezone.localtime(contest.end_time - timedelta(seconds=1)).date() + end_date = timezone.localtime( + contest.end_time - timedelta(seconds=1) + ).date() if start_date == end_date: oneday[start_date].append(contest) else: @@ -470,12 +653,25 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): def get_table(self): calendar = Calendar(self.firstweekday).monthdatescalendar(self.year, self.month) - starts, ends, oneday = self.get_contest_data(make_aware(datetime.combine(calendar[0][0], time.min)), - make_aware(datetime.combine(calendar[-1][-1], time.min))) - return [[ContestDay( - date=date, weekday=self.weekday_classes[weekday], is_pad=date.month != self.month, - is_today=date == self.today, starts=starts[date], ends=ends[date], oneday=oneday[date], - ) for weekday, date in enumerate(week)] for week in calendar] + starts, ends, oneday = self.get_contest_data( + make_aware(datetime.combine(calendar[0][0], time.min)), + make_aware(datetime.combine(calendar[-1][-1], time.min)), + ) + return [ + [ + ContestDay( + date=date, + weekday=self.weekday_classes[weekday], + is_pad=date.month != self.month, + is_today=date == self.today, + starts=starts[date], + ends=ends[date], + oneday=oneday[date], + ) + for weekday, date in enumerate(week) + ] + for week in calendar + ] def get_context_data(self, **kwargs): context = super(ContestCalendar, self).get_context_data(**kwargs) @@ -485,40 +681,53 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView): except ValueError: raise Http404() else: - context['title'] = _('Contests in %(month)s') % {'month': date_filter(month, _("F Y"))} + context["title"] = _("Contests in %(month)s") % { + "month": date_filter(month, _("F Y")) + } - dates = Contest.objects.aggregate(min=Min('start_time'), max=Max('end_time')) + dates = Contest.objects.aggregate(min=Min("start_time"), max=Max("end_time")) min_month = (self.today.year, self.today.month) - if dates['min'] is not None: - min_month = dates['min'].year, dates['min'].month + if dates["min"] is not None: + min_month = dates["min"].year, dates["min"].month max_month = (self.today.year, self.today.month) - if dates['max'] is not None: - max_month = max((dates['max'].year, dates['max'].month), (self.today.year, self.today.month)) + if dates["max"] is not None: + max_month = max( + (dates["max"].year, dates["max"].month), + (self.today.year, self.today.month), + ) month = (self.year, self.month) if month < min_month or month > max_month: # 404 is valid because it merely declares the lack of existence, without any reason raise Http404() - context['now'] = timezone.now() - context['calendar'] = self.get_table() - context['curr_month'] = date(self.year, self.month, 1) + context["now"] = timezone.now() + context["calendar"] = self.get_table() + context["curr_month"] = date(self.year, self.month, 1) if month > min_month: - context['prev_month'] = date(self.year - (self.month == 1), 12 if self.month == 1 else self.month - 1, 1) + context["prev_month"] = date( + self.year - (self.month == 1), + 12 if self.month == 1 else self.month - 1, + 1, + ) else: - context['prev_month'] = None + context["prev_month"] = None if month < max_month: - context['next_month'] = date(self.year + (self.month == 12), 1 if self.month == 12 else self.month + 1, 1) + context["next_month"] = date( + self.year + (self.month == 12), + 1 if self.month == 12 else self.month + 1, + 1, + ) else: - context['next_month'] = None + context["next_month"] = None return context class CachedContestCalendar(ContestCalendar): def render(self): - key = 'contest_cal:%d:%d' % (self.year, self.month) + key = "contest_cal:%d:%d" % (self.year, self.month) cached = cache.get(key) if cached is not None: return HttpResponse(cached) @@ -529,11 +738,11 @@ class CachedContestCalendar(ContestCalendar): class ContestStats(TitleMixin, ContestMixin, DetailView): - template_name = 'contest/stats.html' - POINT_BIN = 10 # in point distribution + template_name = "contest/stats.html" + POINT_BIN = 10 # in point distribution def get_title(self): - return _('%s Statistics') % self.object.name + return _("%s Statistics") % self.object.name def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -543,15 +752,22 @@ class ContestStats(TitleMixin, ContestMixin, DetailView): queryset = Submission.objects.filter(contest_object=self.object) - ac_count = Count(Case(When(result='AC', then=Value(1)), output_field=IntegerField())) - ac_rate = CombinedExpression(ac_count / Count('problem'), '*', Value(100.0), output_field=FloatField()) + ac_count = Count( + Case(When(result="AC", then=Value(1)), output_field=IntegerField()) + ) + ac_rate = CombinedExpression( + ac_count / Count("problem"), "*", Value(100.0), output_field=FloatField() + ) status_count_queryset = list( - queryset.values('problem__code', 'result').annotate(count=Count('result')) - .values_list('problem__code', 'result', 'count'), + queryset.values("problem__code", "result") + .annotate(count=Count("result")) + .values_list("problem__code", "result", "count"), ) labels, codes = [], [] - contest_problems = self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code') + contest_problems = self.object.contest_problems.order_by("order").values_list( + "problem__name", "problem__code" + ) if contest_problems: labels, codes = zip(*contest_problems) num_problems = len(labels) @@ -562,65 +778,85 @@ class ContestStats(TitleMixin, ContestMixin, DetailView): result_data = defaultdict(partial(list, [0] * num_problems)) for i in range(num_problems): - for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']: - result_data[category['code']][i] = category['count'] + 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')) + 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 + 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) + 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]))] - + problem_points[i] = [ + (j * 100 / self.POINT_BIN, counter[i][j]) + for j in range(len(counter[i])) + ] + stats = { - 'problem_status_count': { - 'labels': labels, - 'datasets': [ + "problem_status_count": { + "labels": labels, + "datasets": [ { - 'label': name, - 'backgroundColor': settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[name], - 'data': data, + "label": name, + "backgroundColor": settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[ + name + ], + "data": data, } for name, data in result_data.items() ], }, - 'problem_ac_rate': get_bar_chart( - queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate) - .order_by('contest__problem__order').values_list('problem__name', 'ac_rate'), + "problem_ac_rate": get_bar_chart( + 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) + "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'), + "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"), ), - 'language_ac_rate': get_bar_chart( - queryset.values('language__name').annotate(ac_rate=ac_rate) - .filter(ac_rate__gt=0).values_list('language__name', 'ac_rate'), + "language_ac_rate": get_bar_chart( + queryset.values("language__name") + .annotate(ac_rate=ac_rate) + .filter(ac_rate__gt=0) + .values_list("language__name", "ac_rate"), ), } - context['stats'] = mark_safe(json.dumps(stats)) - context['problems'] = labels + context["stats"] = mark_safe(json.dumps(stats)) + context["problems"] = labels return context ContestRankingProfile = namedtuple( - 'ContestRankingProfile', - 'id user css_class username points cumtime tiebreaker organization participation ' - 'participation_rating problem_cells result_cell', + "ContestRankingProfile", + "id user css_class username points cumtime tiebreaker organization participation " + "participation_rating problem_cells result_cell", ) -BestSolutionData = namedtuple('BestSolutionData', 'code points time state is_pretested') +BestSolutionData = namedtuple("BestSolutionData", "code points time state is_pretested") def make_contest_ranking_profile(contest, participation, contest_problems): @@ -634,33 +870,57 @@ def make_contest_ranking_profile(contest, participation, contest_problems): 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) - for contest_problem in contest_problems], + participation_rating=participation.rating.rating + if hasattr(participation, "rating") + else None, + problem_cells=[ + contest.format.display_user_problem(participation, contest_problem) + for contest_problem in contest_problems + ], result_cell=contest.format.display_participation_result(participation), participation=participation, ) def base_contest_ranking_list(contest, problems, queryset): - return [make_contest_ranking_profile(contest, participation, problems) for participation in - queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')] + return [ + make_contest_ranking_profile(contest, participation, problems) + for participation in queryset.select_related("user__user", "rating").defer( + "user__about", "user__organizations__about" + ) + ] 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', 'tiebreaker')) + 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", "tiebreaker"), + ) -def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list, - show_current_virtual=False, ranker=ranker): - problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order')) +def get_contest_ranking_list( + request, + contest, + participation=None, + ranking_list=contest_ranking_list, + show_current_virtual=False, + ranker=ranker, +): + problems = list( + contest.contest_problems.select_related("problem") + .defer("problem__description") + .order_by("order") + ) - users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker')) + users = ranker( + ranking_list(contest, problems), + key=attrgetter("points", "cumtime", "tiebreaker"), + ) if show_current_virtual: if participation is None and request.user.is_authenticated: @@ -668,38 +928,49 @@ def get_contest_ranking_list(request, contest, participation=None, ranking_list= if participation is None or participation.contest_id != contest.id: participation = None if participation is not None and participation.virtual: - users = chain([('-', make_contest_ranking_profile(contest, participation, problems))], users) + users = chain( + [("-", make_contest_ranking_profile(contest, participation, problems))], + users, + ) return users, problems def contest_ranking_ajax(request, contest, participation=None): contest, exists = _find_contest(request, contest) if not exists: - return HttpResponseBadRequest('Invalid contest', content_type='text/plain') + return HttpResponseBadRequest("Invalid contest", content_type="text/plain") if not contest.can_see_full_scoreboard(request.user): raise Http404() queryset = contest.users.filter(virtual__gte=0) - if request.GET.get('friend') == 'true' and request.profile: + 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': + 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) - }) + 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), + }, + ) class ContestRankingBase(ContestMixin, TitleMixin, DetailView): - template_name = 'contest/ranking.html' + template_name = "contest/ranking.html" tab = None def get_title(self): @@ -718,67 +989,84 @@ class ContestRankingBase(ContestMixin, TitleMixin, DetailView): raise Http404() users, problems = self.get_ranking_list() - context['users'] = users - context['problems'] = problems - context['tab'] = self.tab + context["users"] = users + context["problems"] = problems + context["tab"] = self.tab return context class ContestRanking(ContestRankingBase): - tab = 'ranking' + tab = "ranking" def get_title(self): - return _('%s Rankings') % self.object.name + 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) + queryset = self.object.users.filter( + user=self.request.profile, virtual=ContestParticipation.LIVE + ) return get_contest_ranking_list( - self.request, self.object, + self.request, + self.object, ranking_list=partial(base_contest_ranking_list, queryset=queryset), - ranker=lambda users, key: ((_('???'), user) for user in users), + ranker=lambda users, key: ((_("???"), user) for user in users), ) return get_contest_ranking_list(self.request, self.object) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['has_rating'] = self.object.ratings.exists() + context["has_rating"] = self.object.ratings.exists() return context class ContestParticipationList(LoginRequiredMixin, ContestRankingBase): - tab = 'participation' + tab = "participation" def get_title(self): if self.profile == self.request.profile: - return _('Your participation in %s') % self.object.name + return _("Your participation in %s") % self.object.name 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: + 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('{0}', _('Live'), self.profile.username, - reverse('contest_ranking', args=[self.object.key])) + + queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by( + "-virtual" + ) + live_link = format_html( + '{0}', + _("Live"), + self.profile.username, + reverse("contest_ranking", args=[self.object.key]), + ) return get_contest_ranking_list( - self.request, self.object, show_current_virtual=False, + self.request, + self.object, + show_current_virtual=False, ranking_list=partial(base_contest_ranking_list, queryset=queryset), - ranker=lambda users, key: ((user.participation.virtual or live_link, user) for user in users)) + ranker=lambda users, key: ( + (user.participation.virtual or live_link, user) for user in users + ), + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['has_rating'] = False - context['now'] = timezone.now() - context['rank_header'] = _('Participation') - context['participation_tab'] = True + context["has_rating"] = False + context["now"] = timezone.now() + context["rank_header"] = _("Participation") + context["participation_tab"] = True return context def get(self, request, *args, **kwargs): - if 'user' in kwargs: - self.profile = get_object_or_404(Profile, user__username=kwargs['user']) + if "user" in kwargs: + self.profile = get_object_or_404(Profile, user__username=kwargs["user"]) else: self.profile = self.request.profile return super().get(request, *args, **kwargs) @@ -795,20 +1083,22 @@ class ContestParticipationDisqualify(ContestMixin, SingleObjectMixin, View): self.object = self.get_object() try: - participation = self.object.users.get(pk=request.POST.get('participation')) + participation = self.object.users.get(pk=request.POST.get("participation")) except ObjectDoesNotExist: pass else: participation.set_disqualified(not participation.is_disqualified) - return HttpResponseRedirect(reverse('contest_ranking', args=(self.object.key,))) + return HttpResponseRedirect(reverse("contest_ranking", args=(self.object.key,))) class ContestMossMixin(ContestMixin, PermissionRequiredMixin): - permission_required = 'judge.moss_contest' + permission_required = "judge.moss_contest" def get_object(self, queryset=None): contest = super().get_object(queryset) - if settings.MOSS_API_KEY is None or not contest.is_editable_by(self.request.user): + 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() @@ -816,16 +1106,22 @@ class ContestMossMixin(ContestMixin, PermissionRequiredMixin): class ContestMossView(ContestMossMixin, TitleMixin, DetailView): - template_name = 'contest/moss.html' + template_name = "contest/moss.html" def get_title(self): - return _('%s MOSS Results') % self.object.name + return _("%s MOSS Results") % self.object.name def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - problems = list(map(attrgetter('problem'), self.object.contest_problems.order_by('order') - .select_related('problem'))) + problems = list( + map( + attrgetter("problem"), + self.object.contest_problems.order_by("order").select_related( + "problem" + ), + ) + ) languages = list(map(itemgetter(0), ContestMoss.LANG_MAPPING)) results = ContestMoss.objects.filter(contest=self.object) @@ -836,9 +1132,11 @@ class ContestMossView(ContestMossMixin, TitleMixin, DetailView): for result_list in moss_results.values(): result_list.sort(key=lambda x: languages.index(x.language)) - context['languages'] = languages - context['has_results'] = results.exists() - context['moss_results'] = [(problem, moss_results[problem]) for problem in problems] + context["languages"] = languages + context["has_results"] = results.exists() + context["moss_results"] = [ + (problem, moss_results[problem]) for problem in problems + ] return context @@ -846,8 +1144,9 @@ class ContestMossView(ContestMossMixin, TitleMixin, DetailView): self.object = self.get_object() status = run_moss.delay(self.object.key) return redirect_to_task_status( - status, message=_('Running MOSS for %s...') % (self.object.name,), - redirect=reverse('contest_moss', args=(self.object.key,)), + status, + message=_("Running MOSS for %s...") % (self.object.name,), + redirect=reverse("contest_moss", args=(self.object.key,)), ) @@ -855,40 +1154,45 @@ class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View): def post(self, request, *args, **kwargs): self.object = self.get_object() ContestMoss.objects.filter(contest=self.object).delete() - return HttpResponseRedirect(reverse('contest_moss', args=(self.object.key,))) + return HttpResponseRedirect(reverse("contest_moss", args=(self.object.key,))) class ContestTagDetailAjax(DetailView): model = ContestTag - slug_field = slug_url_kwarg = 'name' - context_object_name = 'tag' - template_name = 'contest/tag-ajax.html' + slug_field = slug_url_kwarg = "name" + context_object_name = "tag" + template_name = "contest/tag-ajax.html" class ContestTagDetail(TitleMixin, ContestTagDetailAjax): - template_name = 'contest/tag.html' + template_name = "contest/tag.html" def get_title(self): - return _('Contest tag: %s') % self.object.name + return _("Contest tag: %s") % self.object.name class ProblemClarificationForm(forms.Form): - body = forms.CharField(widget=HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'), - preview_timeout=1000, hide_preview_button=True)) + 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')}) + self.fields["body"].widget.attrs.update({"placeholder": _("Issue description")}) class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView): form_class = ProblemClarificationForm - template_name = 'contest/clarification.html' + template_name = "contest/clarification.html" def get_form_kwargs(self): kwargs = super(NewContestClarificationView, self).get_form_kwargs() - kwargs['request'] = self.request + kwargs["request"] = self.request return kwargs def is_accessible(self): @@ -898,9 +1202,11 @@ class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView 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() + 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(): @@ -908,28 +1214,34 @@ class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView return super().get(self, request, *args, **kwargs) def form_valid(self, form): - problem_code = self.request.POST['problem'] - description = form.cleaned_data['body'] + 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') + + 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('{1}', reverse('problem_detail', args=[self.object.key]), - self.object.name)) + return mark_safe( + escape(_("New clarification for %s")) + % format_html( + '{1}', + 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') + context["problems"] = ContestProblem.objects.filter( + contest=self.object + ).order_by("order") return context @@ -939,27 +1251,37 @@ class ContestClarificationAjax(ContestMixin, DetailView): 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'])) + polling_time = 1 # minute + last_one_minute = last_five_minutes = timezone.now() - timezone.timedelta( + minutes=polling_time + ) - return JsonResponse(queryset, safe=False, json_dumps_params={'ensure_ascii': False}) + 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} + ) def update_contest_mode(request): - if not request.is_ajax() or not request.method=='POST': - return HttpResponseNotAllowed(['POST']) + if not request.is_ajax() or not request.method == "POST": + return HttpResponseNotAllowed(["POST"]) - old_mode = request.session.get('contest_mode', True) - request.session['contest_mode'] = not old_mode - return HttpResponse() \ No newline at end of file + old_mode = request.session.get("contest_mode", True) + request.session["contest_mode"] = not old_mode + return HttpResponse() diff --git a/judge/views/error.py b/judge/views/error.py index 113422d..cffed7e 100644 --- a/judge/views/error.py +++ b/judge/views/error.py @@ -5,29 +5,42 @@ from django.utils.translation import gettext as _ def error(request, context, status): - return render(request, 'error.html', context=context, status=status) + return render(request, "error.html", context=context, status=status) def error404(request, exception=None): # TODO: "panic: go back" - return render(request, 'generic-message.html', { - 'title': _('404 error'), - 'message': _('Could not find page "%s"') % request.path, - }, status=404) + return render( + request, + "generic-message.html", + { + "title": _("404 error"), + "message": _('Could not find page "%s"') % request.path, + }, + status=404, + ) def error403(request, exception=None): - return error(request, { - 'id': 'unauthorized_access', - 'description': _('no permission for %s') % request.path, - 'code': 403, - }, 403) + return error( + request, + { + "id": "unauthorized_access", + "description": _("no permission for %s") % request.path, + "code": 403, + }, + 403, + ) def error500(request): - return error(request, { - 'id': 'invalid_state', - 'description': _('corrupt page %s') % request.path, - 'traceback': traceback.format_exc(), - 'code': 500, - }, 500) + return error( + request, + { + "id": "invalid_state", + "description": _("corrupt page %s") % request.path, + "traceback": traceback.format_exc(), + "code": 500, + }, + 500, + ) diff --git a/judge/views/internal.py b/judge/views/internal.py index dd19e88..07fa75b 100644 --- a/judge/views/internal.py +++ b/judge/views/internal.py @@ -6,30 +6,42 @@ from django.http import HttpResponseForbidden from judge.utils.diggpaginator import DiggPaginator from judge.models import VolunteerProblemVote, Problem + class InternalProblem(ListView): model = Problem - title = _('Internal problems') - template_name = 'internal/base.html' + title = _("Internal problems") + template_name = "internal/base.html" paginate_by = 100 - context_object_name = 'problems' + context_object_name = "problems" + + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans, - allow_empty_first_page=allow_empty_first_page, **kwargs) - def get_queryset(self): - queryset = Problem.objects.annotate(vote_count=Count('volunteer_user_votes')) \ - .filter(vote_count__gte=1).order_by('-vote_count') + queryset = ( + Problem.objects.annotate(vote_count=Count("volunteer_user_votes")) + .filter(vote_count__gte=1) + .order_by("-vote_count") + ) return queryset def get_context_data(self, **kwargs): context = super(InternalProblem, self).get_context_data(**kwargs) - context['page_type'] = 'problem' - context['title'] = self.title + context["page_type"] = "problem" + context["title"] = self.title return context def get(self, request, *args, **kwargs): if request.user.is_superuser: return super(InternalProblem, self).get(request, *args, **kwargs) - return HttpResponseForbidden() \ No newline at end of file + return HttpResponseForbidden() diff --git a/judge/views/language.py b/judge/views/language.py index 87ab8cb..bbdc4bd 100644 --- a/judge/views/language.py +++ b/judge/views/language.py @@ -7,12 +7,12 @@ from judge.utils.views import TitleMixin class LanguageList(TitleMixin, ListView): model = Language - context_object_name = 'languages' - template_name = 'status/language-list.html' - title = gettext_lazy('Runtimes') + context_object_name = "languages" + template_name = "status/language-list.html" + title = gettext_lazy("Runtimes") def get_queryset(self): - queryset = super().get_queryset().prefetch_related('runtimeversion_set') + queryset = super().get_queryset().prefetch_related("runtimeversion_set") if not self.request.user.is_superuser and not self.request.user.is_staff: queryset = queryset.filter(judges__online=True).distinct() return queryset diff --git a/judge/views/license.py b/judge/views/license.py index 77208e6..0e208b7 100644 --- a/judge/views/license.py +++ b/judge/views/license.py @@ -6,9 +6,9 @@ from judge.utils.views import TitleMixin class LicenseDetail(TitleMixin, DetailView): model = License - slug_field = slug_url_kwarg = 'key' - context_object_name = 'license' - template_name = 'license.html' + slug_field = slug_url_kwarg = "key" + context_object_name = "license" + template_name = "license.html" def get_title(self): return self.object.name diff --git a/judge/views/mailgun.py b/judge/views/mailgun.py index 3bd8321..f28589a 100644 --- a/judge/views/mailgun.py +++ b/judge/views/mailgun.py @@ -15,49 +15,77 @@ from registration.models import RegistrationProfile from judge.utils.unicode import utf8bytes -logger = logging.getLogger('judge.mail.activate') +logger = logging.getLogger("judge.mail.activate") class MailgunActivationView(View): - if hasattr(settings, 'MAILGUN_ACCESS_KEY'): + if hasattr(settings, "MAILGUN_ACCESS_KEY"): + def post(self, request, *args, **kwargs): params = request.POST - timestamp = params.get('timestamp', '') - token = params.get('token', '') - signature = params.get('signature', '') + timestamp = params.get("timestamp", "") + token = params.get("token", "") + signature = params.get("signature", "") - logger.debug('Received request: %s', params) + logger.debug("Received request: %s", params) - if signature != hmac.new(key=utf8bytes(settings.MAILGUN_ACCESS_KEY), - msg=utf8bytes('%s%s' % (timestamp, token)), digestmod=hashlib.sha256).hexdigest(): - logger.info('Rejected request: signature: %s, timestamp: %s, token: %s', signature, timestamp, token) + if ( + signature + != hmac.new( + key=utf8bytes(settings.MAILGUN_ACCESS_KEY), + msg=utf8bytes("%s%s" % (timestamp, token)), + digestmod=hashlib.sha256, + ).hexdigest() + ): + logger.info( + "Rejected request: signature: %s, timestamp: %s, token: %s", + signature, + timestamp, + token, + ) raise PermissionDenied() - _, sender = parseaddr(params.get('from')) + _, sender = parseaddr(params.get("from")) if not sender: - logger.info('Rejected invalid sender: %s', params.get('from')) + logger.info("Rejected invalid sender: %s", params.get("from")) return HttpResponse(status=406) try: user = User.objects.get(email__iexact=sender) except (User.DoesNotExist, User.MultipleObjectsReturned): - logger.info('Rejected unknown sender: %s: %s', sender, params.get('from')) + logger.info( + "Rejected unknown sender: %s: %s", sender, params.get("from") + ) return HttpResponse(status=406) try: registration = RegistrationProfile.objects.get(user=user) except RegistrationProfile.DoesNotExist: - logger.info('Rejected sender without RegistrationProfile: %s: %s', sender, params.get('from')) + logger.info( + "Rejected sender without RegistrationProfile: %s: %s", + sender, + params.get("from"), + ) return HttpResponse(status=406) if registration.activated: - logger.info('Rejected activated sender: %s: %s', sender, params.get('from')) + logger.info( + "Rejected activated sender: %s: %s", sender, params.get("from") + ) return HttpResponse(status=406) key = registration.activation_key - if key in params.get('body-plain', '') or key in params.get('body-html', ''): - if RegistrationProfile.objects.activate_user(key, get_current_site(request)): - logger.info('Activated sender: %s: %s', sender, params.get('from')) - return HttpResponse('Activated', status=200) - logger.info('Failed to activate sender: %s: %s', sender, params.get('from')) + if key in params.get("body-plain", "") or key in params.get( + "body-html", "" + ): + if RegistrationProfile.objects.activate_user( + key, get_current_site(request) + ): + logger.info("Activated sender: %s: %s", sender, params.get("from")) + return HttpResponse("Activated", status=200) + logger.info( + "Failed to activate sender: %s: %s", sender, params.get("from") + ) else: - logger.info('Activation key not found: %s: %s', sender, params.get('from')) + logger.info( + "Activation key not found: %s: %s", sender, params.get("from") + ) return HttpResponse(status=406) @method_decorator(csrf_exempt) diff --git a/judge/views/notification.py b/judge/views/notification.py index b0d350f..ea8d55f 100644 --- a/judge/views/notification.py +++ b/judge/views/notification.py @@ -7,46 +7,48 @@ from django.db.models import BooleanField, Value from judge.utils.cachedict import CacheDict from judge.models import Profile, Comment, Notification -__all__ = ['NotificationList'] +__all__ = ["NotificationList"] + class NotificationList(ListView): model = Notification - context_object_name = 'notifications' - template_name = 'notification/list.html' - + context_object_name = "notifications" + template_name = "notification/list.html" + def get_queryset(self): self.unseen_cnt = self.request.profile.count_unseen_notifications - + query = { - 'owner': self.request.profile, + "owner": self.request.profile, } - self.queryset = Notification.objects.filter(**query)\ - .order_by('-time')[:100] \ - .annotate(seen=Value(True, output_field=BooleanField())) - + self.queryset = ( + Notification.objects.filter(**query) + .order_by("-time")[:100] + .annotate(seen=Value(True, output_field=BooleanField())) + ) + # Mark the several first unseen for cnt, q in enumerate(self.queryset): - if (cnt < self.unseen_cnt): + if cnt < self.unseen_cnt: q.seen = False else: break - + return self.queryset - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['unseen_count'] = self.unseen_cnt - context['title'] = _('Notifications (%d unseen)' % context['unseen_count']) - context['has_notifications'] = self.queryset.exists() - context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) + context["unseen_count"] = self.unseen_cnt + context["title"] = _("Notifications (%d unseen)" % context["unseen_count"]) + context["has_notifications"] = self.queryset.exists() + context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page)) return context def get(self, request, *args, **kwargs): ret = super().get(request, *args, **kwargs) - + # update after rendering Notification.objects.filter(owner=self.request.profile).update(read=True) - - return ret + return ret diff --git a/judge/views/organization.py b/judge/views/organization.py index 057e617..2883fc0 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -14,18 +14,45 @@ 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 +from django.views.generic.detail import ( + SingleObjectMixin, + SingleObjectTemplateResponseMixin, +) from reversion import revisions from judge.forms import EditOrganizationForm -from judge.models import BlogPost, Comment, Organization, OrganizationRequest, Problem, Profile, Contest +from judge.models import ( + BlogPost, + Comment, + Organization, + OrganizationRequest, + Problem, + Profile, + Contest, +) from judge.utils.ranker import ranker -from judge.utils.views import TitleMixin, generic_message, QueryStringSortMixin, DiggPaginatorMixin +from judge.utils.views import ( + TitleMixin, + generic_message, + QueryStringSortMixin, + DiggPaginatorMixin, +) + +__all__ = [ + "OrganizationList", + "OrganizationHome", + "OrganizationUsers", + "OrganizationMembershipChange", + "JoinOrganization", + "LeaveOrganization", + "EditOrganization", + "RequestJoinOrganization", + "OrganizationRequestDetail", + "OrganizationRequestView", + "OrganizationRequestLog", + "KickUserWidgetView", +] -__all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'OrganizationMembershipChange', - 'JoinOrganization', 'LeaveOrganization', 'EditOrganization', 'RequestJoinOrganization', - 'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog', - 'KickUserWidgetView'] class OrganizationBase(object): def can_edit_organization(self, org=None): @@ -34,20 +61,25 @@ class OrganizationBase(object): if not self.request.user.is_authenticated: return False profile_id = self.request.profile.id - return org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id + return ( + org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id + ) def is_member(self, org=None): if org is None: org = self.object - return self.request.profile in org if self.request.user.is_authenticated else False + return ( + self.request.profile in org if self.request.user.is_authenticated else False + ) + class OrganizationMixin(OrganizationBase): - context_object_name = 'organization' + context_object_name = "organization" model = Organization def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['logo_override_image'] = self.object.logo_override_image + context["logo_override_image"] = self.object.logo_override_image return context def dispatch(self, request, *args, **kwargs): @@ -56,92 +88,131 @@ class OrganizationMixin(OrganizationBase): except Http404: key = kwargs.get(self.slug_url_kwarg, None) if key: - return generic_message(request, _('No such organization'), - _('Could not find an organization with the key "%s".') % key) + return generic_message( + request, + _("No such organization"), + _('Could not find an organization with the key "%s".') % key, + ) else: - return generic_message(request, _('No such organization'), - _('Could not find such organization.')) - + return generic_message( + request, + _("No such organization"), + _("Could not find such organization."), + ) + class OrganizationDetailView(OrganizationMixin, DetailView): def get(self, request, *args, **kwargs): self.object = self.get_object() - if self.object.slug != kwargs['slug']: - return HttpResponsePermanentRedirect(request.get_full_path().replace(kwargs['slug'], self.object.slug)) + if self.object.slug != kwargs["slug"]: + return HttpResponsePermanentRedirect( + request.get_full_path().replace(kwargs["slug"], self.object.slug) + ) context = self.get_context_data(object=self.object) return self.render_to_response(context) class OrganizationList(TitleMixin, ListView, OrganizationBase): model = Organization - context_object_name = 'organizations' - template_name = 'organization/list.html' - title = gettext_lazy('Organizations') + context_object_name = "organizations" + template_name = "organization/list.html" + title = gettext_lazy("Organizations") def get_queryset(self): - return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member')) + return ( + super(OrganizationList, self) + .get_queryset() + .annotate(member_count=Count("member")) + ) def get_context_data(self, **kwargs): context = super(OrganizationList, self).get_context_data(**kwargs) - context['my_organizations'] = set() + context["my_organizations"] = set() - for organization in context['organizations']: + for organization in context["organizations"]: if self.can_edit_organization(organization) or self.is_member(organization): - context['my_organizations'].add(organization) + context["my_organizations"].add(organization) return context + class OrganizationHome(OrganizationDetailView): - template_name = 'organization/home.html' + template_name = "organization/home.html" def get_context_data(self, **kwargs): context = super(OrganizationHome, self).get_context_data(**kwargs) - context['title'] = self.object.name - context['can_edit'] = self.can_edit_organization() - context['is_member'] = self.is_member() - context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=True, - organizations=self.object) \ - .order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT] - context['new_contests'] = Contest.objects.filter(is_visible=True, is_organization_private=True, - organizations=self.object) \ - .order_by('-end_time', '-id')[:settings.DMOJ_BLOG_NEW_CONTEST_COUNT] + context["title"] = self.object.name + context["can_edit"] = self.can_edit_organization() + context["is_member"] = self.is_member() + context["new_problems"] = Problem.objects.filter( + is_public=True, is_organization_private=True, organizations=self.object + ).order_by("-date", "-id")[: settings.DMOJ_BLOG_NEW_PROBLEM_COUNT] + context["new_contests"] = Contest.objects.filter( + is_visible=True, is_organization_private=True, organizations=self.object + ).order_by("-end_time", "-id")[: settings.DMOJ_BLOG_NEW_CONTEST_COUNT] - context['posts'] = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now(), - is_organization_private=True, organizations=self.object) \ - .order_by('-sticky', '-publish_on') \ - .prefetch_related('authors__user', 'organizations') - context['post_comment_counts'] = { - int(page[2:]): count for page, count in - 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["posts"] = ( + BlogPost.objects.filter( + visible=True, + publish_on__lte=timezone.now(), + is_organization_private=True, + organizations=self.object, + ) + .order_by("-sticky", "-publish_on") + .prefetch_related("authors__user", "organizations") + ) + context["post_comment_counts"] = { + int(page[2:]): count + for page, count in 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() + context["pending_count"] = OrganizationRequest.objects.filter( + state="P", organization=self.object + ).count() return context class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView): - template_name = 'organization/users.html' - all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points')) + template_name = "organization/users.html" + all_sorts = frozenset(("points", "problem_count", "rating", "performance_points")) default_desc = all_sorts - default_sort = '-performance_points' - + default_sort = "-performance_points" + def get_context_data(self, **kwargs): context = super(OrganizationUsers, self).get_context_data(**kwargs) - context['title'] = _('%s Members') % self.object.name - context['partial'] = True - context['is_admin'] = self.can_edit_organization() - context['kick_url'] = reverse('organization_user_kick', args=[self.object.id, self.object.slug]) + context["title"] = _("%s Members") % self.object.name + context["partial"] = True + context["is_admin"] = self.can_edit_organization() + context["kick_url"] = reverse( + "organization_user_kick", args=[self.object.id, self.object.slug] + ) - context['users'] = ranker( - self.get_object().members.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') \ - .only('display_rank', 'user__username', 'points', 'rating', 'performance_points', 'problem_count') + context["users"] = ranker( + self.get_object() + .members.filter(is_unlisted=False) + .order_by(self.order, "id") + .select_related("user") + .only( + "display_rank", + "user__username", + "points", + "rating", + "performance_points", + "problem_count", ) - context['first_page_href'] = '.' + ) + context["first_page_href"] = "." context.update(self.get_sort_context()) return context -class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View): +class OrganizationMembershipChange( + LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View +): def post(self, request, *args, **kwargs): org = self.get_object() response = self.handle(request, org, request.profile) @@ -156,29 +227,42 @@ class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, Single class JoinOrganization(OrganizationMembershipChange): def handle(self, request, org, profile): if profile.organizations.filter(id=org.id).exists(): - return generic_message(request, _('Joining organization'), _('You are already in the organization.')) + return generic_message( + request, + _("Joining organization"), + _("You are already in the organization."), + ) if not org.is_open: - return generic_message(request, _('Joining organization'), _('This organization is not open.')) + return generic_message( + request, _("Joining organization"), _("This organization is not open.") + ) max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT if profile.organizations.filter(is_open=True).count() >= max_orgs: return generic_message( - request, _('Joining organization'), - _('You may not be part of more than {count} public organizations.').format(count=max_orgs), + request, + _("Joining organization"), + _( + "You may not be part of more than {count} public organizations." + ).format(count=max_orgs), ) profile.organizations.add(org) profile.save() - cache.delete(make_template_fragment_key('org_member_count', (org.id,))) + cache.delete(make_template_fragment_key("org_member_count", (org.id,))) class LeaveOrganization(OrganizationMembershipChange): def handle(self, request, org, profile): if not profile.organizations.filter(id=org.id).exists(): - return generic_message(request, _('Leaving organization'), _('You are not in "%s".') % org.short_name) + return generic_message( + request, + _("Leaving organization"), + _('You are not in "%s".') % org.short_name, + ) profile.organizations.remove(org) - cache.delete(make_template_fragment_key('org_member_count', (org.id,))) + cache.delete(make_template_fragment_key("org_member_count", (org.id,))) class OrganizationRequestForm(Form): @@ -187,9 +271,9 @@ class OrganizationRequestForm(Form): class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView): model = Organization - slug_field = 'key' - slug_url_kwarg = 'key' - template_name = 'organization/requests/request.html' + slug_field = "key" + slug_url_kwarg = "key" + template_name = "organization/requests/request.html" form_class = OrganizationRequestForm def dispatch(self, request, *args, **kwargs): @@ -200,75 +284,99 @@ class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView): context = super(RequestJoinOrganization, self).get_context_data(**kwargs) if self.object.is_open: raise Http404() - context['title'] = _('Request to join %s') % self.object.name + context["title"] = _("Request to join %s") % self.object.name return context def form_valid(self, form): request = OrganizationRequest() request.organization = self.get_object() request.user = self.request.profile - request.reason = form.cleaned_data['reason'] - request.state = 'P' + request.reason = form.cleaned_data["reason"] + request.state = "P" request.save() - return HttpResponseRedirect(reverse('request_organization_detail', args=( - request.organization.id, request.organization.slug, request.id, - ))) + return HttpResponseRedirect( + reverse( + "request_organization_detail", + args=( + request.organization.id, + request.organization.slug, + request.id, + ), + ) + ) class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView): model = OrganizationRequest - template_name = 'organization/requests/detail.html' - title = gettext_lazy('Join request detail') - pk_url_kwarg = 'rpk' + template_name = "organization/requests/detail.html" + title = gettext_lazy("Join request detail") + pk_url_kwarg = "rpk" def get_object(self, queryset=None): object = super(OrganizationRequestDetail, self).get_object(queryset) profile = self.request.profile - if object.user_id != profile.id and not object.organization.admins.filter(id=profile.id).exists(): + if ( + object.user_id != profile.id + and not object.organization.admins.filter(id=profile.id).exists() + ): raise PermissionDenied() return object -OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True) +OrganizationRequestFormSet = modelformset_factory( + OrganizationRequest, extra=0, fields=("state",), can_delete=True +) -class OrganizationRequestBaseView(TitleMixin, LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View): +class OrganizationRequestBaseView( + TitleMixin, + LoginRequiredMixin, + SingleObjectTemplateResponseMixin, + SingleObjectMixin, + View, +): model = Organization - slug_field = 'key' - slug_url_kwarg = 'key' + slug_field = "key" + slug_url_kwarg = "key" tab = None def get_object(self, queryset=None): organization = super(OrganizationRequestBaseView, self).get_object(queryset) - if not (organization.admins.filter(id=self.request.profile.id).exists() or - organization.registrant_id == self.request.profile.id): + if not ( + organization.admins.filter(id=self.request.profile.id).exists() + or organization.registrant_id == self.request.profile.id + ): 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 {self.object.name}') - + href = reverse("organization_home", args=[self.object.id, self.object.slug]) + return mark_safe( + f'Manage join requests for {self.object.name}' + ) + def get_context_data(self, **kwargs): context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs) - context['title'] = _('Managing join requests for %s') % self.object.name - context['tab'] = self.tab + context["title"] = _("Managing join requests for %s") % self.object.name + context["tab"] = self.tab return context class OrganizationRequestView(OrganizationRequestBaseView): - template_name = 'organization/requests/pending.html' - tab = 'pending' + template_name = "organization/requests/pending.html" + tab = "pending" def get_context_data(self, **kwargs): context = super(OrganizationRequestView, self).get_context_data(**kwargs) - context['formset'] = self.formset + context["formset"] = self.formset return context def get(self, request, *args, **kwargs): self.object = self.get_object() self.formset = OrganizationRequestFormSet( - queryset=OrganizationRequest.objects.filter(state='P', organization=self.object), + queryset=OrganizationRequest.objects.filter( + state="P", organization=self.object + ), ) context = self.get_context_data(object=self.object) return self.render_to_response(context) @@ -279,24 +387,43 @@ class OrganizationRequestView(OrganizationRequestBaseView): if formset.is_valid(): if organization.slots is not None: deleted_set = set(formset.deleted_forms) - to_approve = sum(form.cleaned_data['state'] == 'A' for form in formset.forms if form not in deleted_set) + to_approve = sum( + form.cleaned_data["state"] == "A" + for form in formset.forms + if form not in deleted_set + ) can_add = organization.slots - organization.members.count() if to_approve > can_add: - messages.error(request, _('Your organization can only receive %d more members. ' - 'You cannot approve %d users.') % (can_add, to_approve)) - return self.render_to_response(self.get_context_data(object=organization)) + messages.error( + request, + _( + "Your organization can only receive %d more members. " + "You cannot approve %d users." + ) + % (can_add, to_approve), + ) + return self.render_to_response( + self.get_context_data(object=organization) + ) approved, rejected = 0, 0 for obj in formset.save(): - if obj.state == 'A': + if obj.state == "A": obj.user.organizations.add(obj.organization) approved += 1 - elif obj.state == 'R': + elif obj.state == "R": rejected += 1 - messages.success(request, - ungettext('Approved %d user.', 'Approved %d users.', approved) % approved + '\n' + - ungettext('Rejected %d user.', 'Rejected %d users.', rejected) % rejected) - cache.delete(make_template_fragment_key('org_member_count', (organization.id,))) + messages.success( + request, + ungettext("Approved %d user.", "Approved %d users.", approved) + % approved + + "\n" + + ungettext("Rejected %d user.", "Rejected %d users.", rejected) + % rejected, + ) + cache.delete( + make_template_fragment_key("org_member_count", (organization.id,)) + ) return HttpResponseRedirect(request.get_full_path()) return self.render_to_response(self.get_context_data(object=organization)) @@ -304,9 +431,9 @@ class OrganizationRequestView(OrganizationRequestBaseView): class OrganizationRequestLog(OrganizationRequestBaseView): - states = ('A', 'R') - tab = 'log' - template_name = 'organization/requests/log.html' + states = ("A", "R") + tab = "log" + template_name = "organization/requests/log.html" def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -315,17 +442,17 @@ class OrganizationRequestLog(OrganizationRequestBaseView): def get_context_data(self, **kwargs): context = super(OrganizationRequestLog, self).get_context_data(**kwargs) - context['requests'] = self.object.requests.filter(state__in=self.states) + context["requests"] = self.object.requests.filter(state__in=self.states) return context class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, UpdateView): - template_name = 'organization/edit.html' + template_name = "organization/edit.html" model = Organization form_class = EditOrganizationForm def get_title(self): - return _('Editing %s') % self.object.name + return _("Editing %s") % self.object.name def get_object(self, queryset=None): object = super(EditOrganization, self).get_object() @@ -335,13 +462,14 @@ class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, Update def get_form(self, form_class=None): form = super(EditOrganization, self).get_form(form_class) - form.fields['admins'].queryset = \ - Profile.objects.filter(Q(organizations=self.object) | Q(admin_of=self.object)).distinct() + form.fields["admins"].queryset = Profile.objects.filter( + Q(organizations=self.object) | Q(admin_of=self.object) + ).distinct() return form def form_valid(self, form): with transaction.atomic(), revisions.create_revision(): - revisions.set_comment(_('Edited from site')) + revisions.set_comment(_("Edited from site")) revisions.set_user(self.request.user) return super(EditOrganization, self).form_valid(form) @@ -349,27 +477,45 @@ class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, Update try: return super(EditOrganization, self).dispatch(request, *args, **kwargs) except PermissionDenied: - return generic_message(request, _("Can't edit organization"), - _('You are not allowed to edit this organization.'), status=403) + return generic_message( + request, + _("Can't edit organization"), + _("You are not allowed to edit this organization."), + status=403, + ) -class KickUserWidgetView(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View): +class KickUserWidgetView( + LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View +): def post(self, request, *args, **kwargs): organization = self.get_object() if not self.can_edit_organization(organization): - return generic_message(request, _("Can't edit organization"), - _('You are not allowed to kick people from this organization.'), status=403) + return generic_message( + request, + _("Can't edit organization"), + _("You are not allowed to kick people from this organization."), + status=403, + ) try: - user = Profile.objects.get(id=request.POST.get('user', None)) + user = Profile.objects.get(id=request.POST.get("user", None)) except Profile.DoesNotExist: - return generic_message(request, _("Can't kick user"), - _('The user you are trying to kick does not exist!'), status=400) + return generic_message( + request, + _("Can't kick user"), + _("The user you are trying to kick does not exist!"), + status=400, + ) if not organization.members.filter(id=user.id).exists(): - return generic_message(request, _("Can't kick user"), - _('The user you are trying to kick is not in organization: %s.') % - organization.name, status=400) + return generic_message( + request, + _("Can't kick user"), + _("The user you are trying to kick is not in organization: %s.") + % organization.name, + status=400, + ) organization.members.remove(user) return HttpResponseRedirect(organization.get_users_url()) diff --git a/judge/views/preview.py b/judge/views/preview.py index 5fbd2e8..e4581d4 100644 --- a/judge/views/preview.py +++ b/judge/views/preview.py @@ -5,46 +5,48 @@ from django.views.generic.base import ContextMixin, TemplateResponseMixin, View class MarkdownPreviewView(TemplateResponseMixin, ContextMixin, View): def post(self, request, *args, **kwargs): try: - self.preview_data = data = request.POST['preview'] + self.preview_data = data = request.POST["preview"] except KeyError: - return HttpResponseBadRequest('No preview data specified.') + return HttpResponseBadRequest("No preview data specified.") - return self.render_to_response(self.get_context_data( - preview_data=data, - )) + return self.render_to_response( + self.get_context_data( + preview_data=data, + ) + ) class ProblemMarkdownPreviewView(MarkdownPreviewView): - template_name = 'problem/preview.html' + template_name = "problem/preview.html" class BlogMarkdownPreviewView(MarkdownPreviewView): - template_name = 'blog/preview.html' + template_name = "blog/preview.html" class ContestMarkdownPreviewView(MarkdownPreviewView): - template_name = 'contest/preview.html' + template_name = "contest/preview.html" class CommentMarkdownPreviewView(MarkdownPreviewView): - template_name = 'comments/preview.html' + template_name = "comments/preview.html" class ProfileMarkdownPreviewView(MarkdownPreviewView): - template_name = 'user/preview.html' + template_name = "user/preview.html" class OrganizationMarkdownPreviewView(MarkdownPreviewView): - template_name = 'organization/preview.html' + template_name = "organization/preview.html" class SolutionMarkdownPreviewView(MarkdownPreviewView): - template_name = 'solution-preview.html' + template_name = "solution-preview.html" class LicenseMarkdownPreviewView(MarkdownPreviewView): - template_name = 'license-preview.html' + template_name = "license-preview.html" class TicketMarkdownPreviewView(MarkdownPreviewView): - template_name = 'ticket/preview.html' + template_name = "ticket/preview.html" diff --git a/judge/views/problem.py b/judge/views/problem.py index a00e9df..08bcb54 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -13,7 +13,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import Count, F, Prefetch, Q, Sum, Case, When, IntegerField from django.db.utils import ProgrammingError -from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect, JsonResponse +from django.http import ( + Http404, + HttpResponse, + HttpResponseForbidden, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import get_object_or_404, render from django.template.loader import get_template from django.urls import reverse @@ -28,17 +34,43 @@ from django.views.generic.detail import SingleObjectMixin from judge.comments import CommentedDetailView from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm -from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \ - ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \ - TranslatedProblemForeignKeyQuerySet, Organization , VolunteerProblemVote +from judge.models import ( + ContestProblem, + ContestSubmission, + Judge, + Language, + Problem, + ProblemClarification, + ProblemGroup, + ProblemTranslation, + ProblemType, + ProblemPointsVote, + RuntimeVersion, + Solution, + Submission, + SubmissionSource, + TranslatedProblemForeignKeyQuerySet, + Organization, + VolunteerProblemVote, +) from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.utils.diggpaginator import DiggPaginator from judge.utils.opengraph import generate_opengraph -from judge.utils.problems import contest_attempted_ids, contest_completed_ids, hot_problems, user_attempted_ids, \ - user_completed_ids +from judge.utils.problems import ( + contest_attempted_ids, + contest_completed_ids, + hot_problems, + user_attempted_ids, + user_completed_ids, +) from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.tickets import own_ticket_filter -from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message +from judge.utils.views import ( + QueryStringSortMixin, + SingleObjectFormView, + TitleMixin, + generic_message, +) from judge.ml.collab_filter import CollabFilter @@ -50,14 +82,17 @@ def get_contest_problem(problem, profile): def get_contest_submission_count(problem, profile, virtual): - return profile.current_contest.submissions.exclude(submission__status__in=['IE']) \ - .filter(problem__problem=problem, participation__virtual=virtual).count() + return ( + profile.current_contest.submissions.exclude(submission__status__in=["IE"]) + .filter(problem__problem=problem, participation__virtual=virtual) + .count() + ) class ProblemMixin(object): model = Problem - slug_url_kwarg = 'problem' - slug_field = 'code' + slug_url_kwarg = "problem" + slug_field = "code" def get_object(self, queryset=None): problem = super(ProblemMixin, self).get_object(queryset) @@ -67,8 +102,12 @@ class ProblemMixin(object): def no_such_problem(self): code = self.kwargs.get(self.slug_url_kwarg, None) - return generic_message(self.request, _('No such problem'), - _('Could not find a problem with the code "%s".') % code, status=404) + return generic_message( + self.request, + _("No such problem"), + _('Could not find a problem with the code "%s".') % code, + status=404, + ) def get(self, request, *args, **kwargs): try: @@ -93,8 +132,11 @@ class SolvedProblemMixin(object): @cached_property def in_contest(self): - return self.profile is not None and self.profile.current_contest is not None \ + return ( + self.profile is not None + and self.profile.current_contest is not None and self.request.in_contest_mode + ) @cached_property def contest(self): @@ -107,174 +149,221 @@ class SolvedProblemMixin(object): return self.request.profile -class ProblemSolution(SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView): - context_object_name = 'problem' - template_name = 'problem/editorial.html' +class ProblemSolution( + SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView +): + context_object_name = "problem" + template_name = "problem/editorial.html" def get_title(self): - return _('Editorial for {0}').format(self.object.name) + return _("Editorial for {0}").format(self.object.name) def get_content_title(self): - return format_html(_(u'Editorial for {0}'), self.object.name, - reverse('problem_detail', args=[self.object.code])) + return format_html( + _('Editorial for {0}'), + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) def get_context_data(self, **kwargs): context = super(ProblemSolution, self).get_context_data(**kwargs) solution = get_object_or_404(Solution, problem=self.object) - if (not solution.is_public or solution.publish_on > timezone.now()) and \ - not self.request.user.has_perm('judge.see_private_solution'): + if ( + not solution.is_public or solution.publish_on > timezone.now() + ) and not self.request.user.has_perm("judge.see_private_solution"): raise Http404() - context['solution'] = solution - context['has_solved_problem'] = self.object.id in self.get_completed_problems() + context["solution"] = solution + context["has_solved_problem"] = self.object.id in self.get_completed_problems() return context def get_comment_page(self): - return 's:' + self.object.code + return "s:" + self.object.code -class ProblemRaw(ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMixin, View): - context_object_name = 'problem' - template_name = 'problem/raw.html' +class ProblemRaw( + ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMixin, View +): + context_object_name = "problem" + template_name = "problem/raw.html" def get_title(self): return self.object.name def get_context_data(self, **kwargs): context = super(ProblemRaw, self).get_context_data(**kwargs) - context['problem_name'] = self.object.name - context['url'] = self.request.build_absolute_uri() - context['description'] = self.object.description + context["problem_name"] = self.object.name + context["url"] = self.request.build_absolute_uri() + context["description"] = self.object.description return context def get(self, request, *args, **kwargs): self.object = self.get_object() with translation.override(settings.LANGUAGE_CODE): - return self.render_to_response(self.get_context_data( - object=self.object, - )) + return self.render_to_response( + self.get_context_data( + object=self.object, + ) + ) class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): - context_object_name = 'problem' - template_name = 'problem/problem.html' + context_object_name = "problem" + template_name = "problem/problem.html" def get_comment_page(self): - return 'p:%s' % self.object.code + return "p:%s" % self.object.code def get_context_data(self, **kwargs): context = super(ProblemDetail, self).get_context_data(**kwargs) user = self.request.user authed = user.is_authenticated - context['has_submissions'] = authed and Submission.objects.filter(user=user.profile, - problem=self.object).exists() - 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 + context["has_submissions"] = ( + authed + and Submission.objects.filter( + user=user.profile, problem=self.object + ).exists() + ) + 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 - context['clarifications'] = clarifications.order_by('-date') - context['submission_limit'] = contest_problem.max_submissions + context["has_clarifications"] = clarifications.count() > 0 + context["clarifications"] = clarifications.order_by("-date") + context["submission_limit"] = contest_problem.max_submissions if contest_problem.max_submissions: - context['submissions_left'] = max(contest_problem.max_submissions - - get_contest_submission_count(self.object, user.profile, - user.profile.current_contest.virtual), 0) + context["submissions_left"] = max( + contest_problem.max_submissions + - get_contest_submission_count( + self.object, user.profile, user.profile.current_contest.virtual + ), + 0, + ) - context['available_judges'] = Judge.objects.filter(online=True, problems=self.object) - context['show_languages'] = self.object.allowed_languages.count() != Language.objects.count() - context['has_pdf_render'] = HAS_PDF - context['completed_problem_ids'] = self.get_completed_problems() - context['attempted_problems'] = self.get_attempted_problems() + context["available_judges"] = Judge.objects.filter( + online=True, problems=self.object + ) + context["show_languages"] = ( + self.object.allowed_languages.count() != Language.objects.count() + ) + context["has_pdf_render"] = HAS_PDF + context["completed_problem_ids"] = self.get_completed_problems() + context["attempted_problems"] = self.get_attempted_problems() can_edit = self.object.is_editable_by(user) - context['can_edit_problem'] = can_edit + context["can_edit_problem"] = can_edit if user.is_authenticated: tickets = self.object.tickets if not can_edit: tickets = tickets.filter(own_ticket_filter(user.profile.id)) - context['has_tickets'] = tickets.exists() - context['num_open_tickets'] = tickets.filter(is_open=True).values('id').distinct().count() + context["has_tickets"] = tickets.exists() + context["num_open_tickets"] = ( + tickets.filter(is_open=True).values("id").distinct().count() + ) try: - context['editorial'] = Solution.objects.get(problem=self.object) + context["editorial"] = Solution.objects.get(problem=self.object) except ObjectDoesNotExist: pass try: - translation = self.object.translations.get(language=self.request.LANGUAGE_CODE) + translation = self.object.translations.get( + language=self.request.LANGUAGE_CODE + ) except ProblemTranslation.DoesNotExist: - context['title'] = self.object.name - context['language'] = settings.LANGUAGE_CODE - context['description'] = self.object.description - context['translated'] = False + context["title"] = self.object.name + context["language"] = settings.LANGUAGE_CODE + context["description"] = self.object.description + context["translated"] = False else: - context['title'] = translation.name - context['language'] = self.request.LANGUAGE_CODE - context['description'] = translation.description - context['translated'] = True + context["title"] = translation.name + context["language"] = self.request.LANGUAGE_CODE + context["description"] = translation.description + context["translated"] = True if not self.object.og_image or not self.object.summary: - metadata = generate_opengraph('generated-meta-problem:%s:%d' % (context['language'], self.object.id), - context['description'], 'problem') - context['meta_description'] = self.object.summary or metadata[0] - context['og_image'] = self.object.og_image or metadata[1] + metadata = generate_opengraph( + "generated-meta-problem:%s:%d" % (context["language"], self.object.id), + context["description"], + "problem", + ) + context["meta_description"] = self.object.summary or metadata[0] + context["og_image"] = self.object.og_image or metadata[1] - context['can_vote'] = self.object.can_vote(self.request) - if context['can_vote']: + context["can_vote"] = self.object.can_vote(self.request) + if context["can_vote"]: try: - context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object) + context["vote"] = ProblemPointsVote.objects.get( + voter=user.profile, problem=self.object + ) except ObjectDoesNotExist: - context['vote'] = None + context["vote"] = None else: - context['vote'] = None + context["vote"] = None - context['has_votes'] = False + context["has_votes"] = False if user.is_superuser: - all_votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True)) - context['all_votes'] = all_votes - context['has_votes'] = len(all_votes) > 0 - context['max_possible_vote'] = 600 - context['min_possible_vote'] = 100 + all_votes = list( + self.object.problem_points_votes.order_by("points").values_list( + "points", flat=True + ) + ) + context["all_votes"] = all_votes + context["has_votes"] = len(all_votes) > 0 + context["max_possible_vote"] = 600 + context["min_possible_vote"] = 100 return context class DeleteVote(ProblemMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): - return HttpResponseForbidden(status=405, content_type='text/plain') + return HttpResponseForbidden(status=405, content_type="text/plain") def post(self, request, *args, **kwargs): self.object = self.get_object() if not request.user.is_authenticated: - return HttpResponseForbidden('Not signed in.', content_type='text/plain') + return HttpResponseForbidden("Not signed in.", content_type="text/plain") elif self.object.can_vote(request.user): - ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete() - return HttpResponse('success', content_type='text/plain') + ProblemPointsVote.objects.filter( + voter=request.profile, problem=self.object + ).delete() + return HttpResponse("success", content_type="text/plain") else: - return HttpResponseForbidden('Not allowed to delete votes on this problem.', content_type='text/plain') + return HttpResponseForbidden( + "Not allowed to delete votes on this problem.", + content_type="text/plain", + ) class Vote(ProblemMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): - return HttpResponseForbidden(status=405, content_type='text/plain') + return HttpResponseForbidden(status=405, content_type="text/plain") def post(self, request, *args, **kwargs): self.object = self.get_object() if not self.object.can_vote(request): # Not allowed to vote for some reason. - return HttpResponseForbidden('Not allowed to vote on this problem.', content_type='text/plain') + return HttpResponseForbidden( + "Not allowed to vote on this problem.", content_type="text/plain" + ) form = ProblemPointsVoteForm(request.POST) if form.is_valid(): with transaction.atomic(): # Delete any pre existing votes. - ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete() + ProblemPointsVote.objects.filter( + voter=request.profile, problem=self.object + ).delete() vote = form.save(commit=False) vote.voter = request.profile vote.problem = self.object vote.save() - return JsonResponse({'points': vote.points}) + return JsonResponse({"points": vote.points}) else: return JsonResponse(form.errors, status=400) @@ -284,14 +373,14 @@ class LatexError(Exception): class ProblemPdfView(ProblemMixin, SingleObjectMixin, View): - logger = logging.getLogger('judge.problem.pdf') + logger = logging.getLogger("judge.problem.pdf") languages = set(map(itemgetter(0), settings.LANGUAGES)) def get(self, request, *args, **kwargs): if not HAS_PDF: raise Http404() - language = kwargs.get('language', self.request.LANGUAGE_CODE) + language = kwargs.get("language", self.request.LANGUAGE_CODE) if language not in self.languages: raise Http404() @@ -301,74 +390,103 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View): except ProblemTranslation.DoesNotExist: trans = None - cache = os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, '%s.%s.pdf' % (problem.code, language)) + cache = os.path.join( + settings.DMOJ_PDF_PROBLEM_CACHE, "%s.%s.pdf" % (problem.code, language) + ) if not os.path.exists(cache): - self.logger.info('Rendering: %s.%s.pdf', problem.code, language) + self.logger.info("Rendering: %s.%s.pdf", problem.code, language) with DefaultPdfMaker() as maker, translation.override(language): problem_name = problem.name if trans is None else trans.name - maker.html = get_template('problem/raw.html').render({ - 'problem': problem, - 'problem_name': problem_name, - 'description': problem.description if trans is None else trans.description, - 'url': request.build_absolute_uri(), - 'math_engine': maker.math_engine, - }).replace('"//', '"https://').replace("'//", "'https://") + maker.html = ( + get_template("problem/raw.html") + .render( + { + "problem": problem, + "problem_name": problem_name, + "description": problem.description + if trans is None + else trans.description, + "url": request.build_absolute_uri(), + "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') + assets = ["style.css", "pygment-github.css"] + if maker.math_engine == "jax": + assets.append("mathjax_config.js") for file in assets: maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.make() if not maker.success: - self.logger.error('Failed to render PDF for %s', problem.code) - return HttpResponse(maker.log, status=500, content_type='text/plain') + 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/'): - response['X-Accel-Redirect'] = '%s/%s.%s.pdf' % (settings.DMOJ_PDF_PROBLEM_INTERNAL, problem.code, language) + if hasattr(settings, "DMOJ_PDF_PROBLEM_INTERNAL") and request.META.get( + "SERVER_SOFTWARE", "" + ).startswith("nginx/"): + response["X-Accel-Redirect"] = "%s/%s.%s.pdf" % ( + settings.DMOJ_PDF_PROBLEM_INTERNAL, + problem.code, + language, + ) else: - with open(cache, 'rb') as f: + with open(cache, "rb") as f: response.content = f.read() - response['Content-Type'] = 'application/pdf' - response['Content-Disposition'] = 'inline; filename=%s.%s.pdf' % (problem.code, language) + response["Content-Type"] = "application/pdf" + response["Content-Disposition"] = "inline; filename=%s.%s.pdf" % ( + problem.code, + language, + ) return response class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView): model = Problem - title = gettext_lazy('Problems') - context_object_name = 'problems' - template_name = 'problem/list.html' + title = gettext_lazy("Problems") + context_object_name = "problems" + template_name = "problem/list.html" paginate_by = 50 - sql_sort = frozenset(('date', 'points', 'ac_rate', 'user_count', 'code')) - manual_sort = frozenset(('name', 'group', 'solved', 'type')) + sql_sort = frozenset(("date", "points", "ac_rate", "user_count", "code")) + manual_sort = frozenset(("name", "group", "solved", "type")) all_sorts = sql_sort | manual_sort - default_desc = frozenset(('date', 'points', 'ac_rate', 'user_count')) - default_sort = '-date' + default_desc = frozenset(("date", "points", "ac_rate", "user_count")) + default_sort = "-date" first_page_href = None - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - paginator = DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans, - allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + paginator = DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) if not self.in_contest: # Get the number of pages and then add in this magic. # noinspection PyStatementEffect paginator.num_pages queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) - sort_key = self.order.lstrip('-') + sort_key = self.order.lstrip("-") if sort_key in self.sql_sort: queryset = queryset.order_by(self.order) - elif sort_key == 'name': - queryset = queryset.order_by(self.order.replace('name', 'i18n_name')) - elif sort_key == 'group': - queryset = queryset.order_by(self.order + '__name') - elif sort_key == 'solved': + elif sort_key == "name": + queryset = queryset.order_by(self.order.replace("name", "i18n_name")) + elif sort_key == "group": + queryset = queryset.order_by(self.order + "__name") + elif sort_key == "solved": if self.request.user.is_authenticated: profile = self.request.profile solved = user_completed_ids(profile) @@ -382,12 +500,18 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return -1 queryset = list(queryset) - queryset.sort(key=_solved_sort_order, reverse=self.order.startswith('-')) - elif sort_key == 'type': + queryset.sort( + key=_solved_sort_order, reverse=self.order.startswith("-") + ) + elif sort_key == "type": if self.show_types: queryset = list(queryset) - queryset.sort(key=lambda problem: problem.types_list[0] if problem.types_list else '', - reverse=self.order.startswith('-')) + queryset.sort( + key=lambda problem: problem.types_list[0] + if problem.types_list + else "", + reverse=self.order.startswith("-"), + ) paginator.object_list = queryset return paginator @@ -398,24 +522,40 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return self.request.profile def get_contest_queryset(self): - queryset = self.profile.current_contest.contest.contest_problems.select_related('problem__group') \ - .defer('problem__description').order_by('problem__code') \ - .annotate(user_count=Count('submission__participation', distinct=True)) \ - .order_by('order') - queryset = TranslatedProblemForeignKeyQuerySet.add_problem_i18n_name(queryset, 'i18n_name', - self.request.LANGUAGE_CODE, - 'problem__name') - return [{ - 'id': p['problem_id'], - 'code': p['problem__code'], - 'name': p['problem__name'], - 'i18n_name': p['i18n_name'], - 'group': {'full_name': p['problem__group__full_name']}, - 'points': p['points'], - 'partial': p['partial'], - 'user_count': p['user_count'], - } for p in queryset.values('problem_id', 'problem__code', 'problem__name', 'i18n_name', - 'problem__group__full_name', 'points', 'partial', 'user_count')] + queryset = ( + self.profile.current_contest.contest.contest_problems.select_related( + "problem__group" + ) + .defer("problem__description") + .order_by("problem__code") + .annotate(user_count=Count("submission__participation", distinct=True)) + .order_by("order") + ) + queryset = TranslatedProblemForeignKeyQuerySet.add_problem_i18n_name( + queryset, "i18n_name", self.request.LANGUAGE_CODE, "problem__name" + ) + return [ + { + "id": p["problem_id"], + "code": p["problem__code"], + "name": p["problem__name"], + "i18n_name": p["i18n_name"], + "group": {"full_name": p["problem__group__full_name"]}, + "points": p["points"], + "partial": p["partial"], + "user_count": p["user_count"], + } + for p in queryset.values( + "problem_id", + "problem__code", + "problem__name", + "i18n_name", + "problem__group__full_name", + "points", + "partial", + "user_count", + ) + ] def get_normal_queryset(self): filter = Q(is_public=True) @@ -423,42 +563,63 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView filter |= Q(authors=self.profile) filter |= Q(curators=self.profile) filter |= Q(testers=self.profile) - queryset = Problem.objects.filter(filter).select_related('group').defer('description') - if not self.request.user.has_perm('see_organization_problem'): + queryset = ( + Problem.objects.filter(filter).select_related("group").defer("description") + ) + if not self.request.user.has_perm("see_organization_problem"): filter = Q(is_organization_private=False) if self.profile is not None: filter |= Q(organizations__in=self.profile.organizations.all()) queryset = queryset.filter(filter) if self.profile is not None and self.hide_solved: - queryset = queryset.exclude(id__in=Submission.objects.filter(user=self.profile, points=F('problem__points')) - .values_list('problem__id', flat=True)) + queryset = queryset.exclude( + id__in=Submission.objects.filter( + user=self.profile, points=F("problem__points") + ).values_list("problem__id", flat=True) + ) if self.org_query: queryset = queryset.filter( - Q(organizations__in=self.org_query) | - Q(contests__contest__organizations__in=self.org_query)) + Q(organizations__in=self.org_query) + | Q(contests__contest__organizations__in=self.org_query) + ) if self.show_types: - queryset = queryset.prefetch_related('types') + queryset = queryset.prefetch_related("types") if self.category is not None: queryset = queryset.filter(group__id=self.category) if self.selected_types: queryset = queryset.filter(types__in=self.selected_types) - if 'search' in self.request.GET: - self.search_query = query = ' '.join(self.request.GET.getlist('search')).strip() + if "search" in self.request.GET: + self.search_query = query = " ".join( + self.request.GET.getlist("search") + ).strip() if query: if settings.ENABLE_FTS and self.full_text: - queryset = queryset.search(query, queryset.BOOLEAN).extra(order_by=['-relevance']) + queryset = queryset.search(query, queryset.BOOLEAN).extra( + order_by=["-relevance"] + ) else: queryset = queryset.filter( - Q(code__icontains=query) | Q(name__icontains=query) | - Q(translations__name__icontains=query, translations__language=self.request.LANGUAGE_CODE)) + Q(code__icontains=query) + | Q(name__icontains=query) + | Q( + translations__name__icontains=query, + translations__language=self.request.LANGUAGE_CODE, + ) + ) self.prepoint_queryset = queryset if self.point_start is not None: queryset = queryset.filter(points__gte=self.point_start) if self.point_end is not None: queryset = queryset.filter(points__lte=self.point_end) queryset = queryset.annotate( - has_public_editorial=Sum(Case(When(solution__is_public=True, then=1), - default=0, output_field=IntegerField()))) + has_public_editorial=Sum( + Case( + When(solution__is_public=True, then=1), + default=0, + output_field=IntegerField(), + ) + ) + ) return queryset.distinct() @@ -470,82 +631,108 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView def get_context_data(self, **kwargs): context = super(ProblemList, self).get_context_data(**kwargs) - - context['hide_solved'] = 0 if self.in_contest else int(self.hide_solved) - context['show_types'] = 0 if self.in_contest else int(self.show_types) - context['full_text'] = 0 if self.in_contest else int(self.full_text) - context['show_editorial'] = 0 if self.in_contest else int(self.show_editorial) - context['have_editorial'] = 0 if self.in_contest else int(self.have_editorial) - context['organizations'] = Organization.objects.all() - context['category'] = self.category - context['categories'] = ProblemGroup.objects.all() + context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved) + context["show_types"] = 0 if self.in_contest else int(self.show_types) + context["full_text"] = 0 if self.in_contest else int(self.full_text) + context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial) + context["have_editorial"] = 0 if self.in_contest else int(self.have_editorial) + + context["organizations"] = Organization.objects.all() + context["category"] = self.category + context["categories"] = ProblemGroup.objects.all() if self.show_types: - context['selected_types'] = self.selected_types - context['problem_types'] = ProblemType.objects.all() - context['has_fts'] = settings.ENABLE_FTS - context['org_query'] = self.org_query - context['search_query'] = self.search_query - context['completed_problem_ids'] = self.get_completed_problems() - context['attempted_problems'] = self.get_attempted_problems() - context['page_type'] = 'list' + context["selected_types"] = self.selected_types + context["problem_types"] = ProblemType.objects.all() + context["has_fts"] = settings.ENABLE_FTS + context["org_query"] = self.org_query + context["search_query"] = self.search_query + context["completed_problem_ids"] = self.get_completed_problems() + context["attempted_problems"] = self.get_attempted_problems() + context["page_type"] = "list" context.update(self.get_sort_paginate_context()) if not self.in_contest: context.update(self.get_sort_context()) - context['point_start'], context['point_end'], context['point_values'] = self.get_noui_slider_points() + ( + context["point_start"], + context["point_end"], + context["point_values"], + ) = self.get_noui_slider_points() else: - context['point_start'], context['point_end'], context['point_values'] = 0, 0, {} - context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \ - (self.contest.SCOREBOARD_AFTER_CONTEST, self.contest.SCOREBOARD_AFTER_PARTICIPATION) - context['has_clarifications'] = False - + context["point_start"], context["point_end"], context["point_values"] = ( + 0, + 0, + {}, + ) + 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') + 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['page_prefix'] = None - context['page_suffix'] = suffix = ( - '?' + self.request.GET.urlencode()) if self.request.GET else '' - context['first_page_href'] = (self.first_page_href or '.') + suffix - + context["can_edit_contest"] = True + + context["page_prefix"] = None + context["page_suffix"] = suffix = ( + ("?" + self.request.GET.urlencode()) if self.request.GET else "" + ) + context["first_page_href"] = (self.first_page_href or ".") + suffix + return context def get_noui_slider_points(self): - points = sorted(self.prepoint_queryset.values_list('points', flat=True).distinct()) + points = sorted( + self.prepoint_queryset.values_list("points", flat=True).distinct() + ) if not points: return 0, 0, {} if len(points) == 1: - return points[0], points[0], { - 'min': points[0] - 1, - 'max': points[0] + 1, - } + return ( + points[0], + points[0], + { + "min": points[0] - 1, + "max": points[0] + 1, + }, + ) start, end = points[0], points[-1] if self.point_start is not None: start = self.point_start if self.point_end is not None: end = self.point_end - points_map = {0.0: 'min', 1.0: 'max'} + points_map = {0.0: "min", 1.0: "max"} size = len(points) - 1 - return start, end, {points_map.get(i / size, '%.2f%%' % (100 * i / size,)): j for i, j in enumerate(points)} + return ( + start, + end, + { + points_map.get(i / size, "%.2f%%" % (100 * i / size,)): j + for i, j in enumerate(points) + }, + ) def GET_with_session(self, request, key): if not request.GET: return request.session.get(key, False) - return request.GET.get(key, None) == '1' + return request.GET.get(key, None) == "1" def setup_problem_list(self, request): - self.hide_solved = self.GET_with_session(request, 'hide_solved') - self.show_types = self.GET_with_session(request, 'show_types') - self.full_text = self.GET_with_session(request, 'full_text') - self.show_editorial = self.GET_with_session(request, 'show_editorial') - self.have_editorial = self.GET_with_session(request, 'have_editorial') - + self.hide_solved = self.GET_with_session(request, "hide_solved") + self.show_types = self.GET_with_session(request, "show_types") + self.full_text = self.GET_with_session(request, "full_text") + self.show_editorial = self.GET_with_session(request, "show_editorial") + self.have_editorial = self.GET_with_session(request, "have_editorial") + self.search_query = None self.category = None self.org_query = [] @@ -554,23 +741,23 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView # This actually copies into the instance dictionary... self.all_sorts = set(self.all_sorts) if not self.show_types: - self.all_sorts.discard('type') + self.all_sorts.discard("type") - self.category = safe_int_or_none(request.GET.get('category')) - if 'type' in request.GET: + self.category = safe_int_or_none(request.GET.get("category")) + if "type" in request.GET: try: - self.selected_types = list(map(int, request.GET.getlist('type'))) + self.selected_types = list(map(int, request.GET.getlist("type"))) except ValueError: pass - if 'orgs' in request.GET: + if "orgs" in request.GET: try: - self.org_query = list(map(int, request.GET.getlist('orgs'))) + self.org_query = list(map(int, request.GET.getlist("orgs"))) except ValueError: pass - self.point_start = safe_float_or_none(request.GET.get('point_start')) - self.point_end = safe_float_or_none(request.GET.get('point_end')) + self.point_start = safe_float_or_none(request.GET.get("point_start")) + self.point_end = safe_float_or_none(request.GET.get("point_end")) def get(self, request, *args, **kwargs): self.setup_problem_list(request) @@ -578,39 +765,52 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView try: return super(ProblemList, self).get(request, *args, **kwargs) except ProgrammingError as e: - return generic_message(request, 'FTS syntax error', e.args[1], status=400) + return generic_message(request, "FTS syntax error", e.args[1], status=400) def post(self, request, *args, **kwargs): - to_update = ('hide_solved', 'show_types', 'full_text', - 'show_editorial', 'have_editorial') + to_update = ( + "hide_solved", + "show_types", + "full_text", + "show_editorial", + "have_editorial", + ) for key in to_update: if key in request.GET: - val = request.GET.get(key) == '1' + val = request.GET.get(key) == "1" request.session[key] = val else: request.session[key] = False return HttpResponseRedirect(request.get_full_path()) -cf_logger = logging.getLogger('judge.ml.collab_filter') +cf_logger = logging.getLogger("judge.ml.collab_filter") class ProblemFeed(ProblemList): model = Problem - context_object_name = 'problems' + context_object_name = "problems" paginate_by = 20 - title = _('Problem feed') + title = _("Problem feed") feed_type = None def GET_with_session(self, request, key): if not request.GET: - return request.session.get(key, key=='hide_solved') - return request.GET.get(key, None) == '1' + return request.session.get(key, key == "hide_solved") + return request.GET.get(key, None) == "1" - def get_paginator(self, queryset, per_page, orphans=0, - allow_empty_first_page=True, **kwargs): - return DiggPaginator(queryset, per_page, body=6, padding=2, - orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) + def get_paginator( + self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs + ): + return DiggPaginator( + queryset, + per_page, + body=6, + padding=2, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + **kwargs + ) # arr = [[], [], ..] def merge_recommendation(self, arr): @@ -633,76 +833,94 @@ class ProblemFeed(ProblemList): def get_queryset(self): queryset = super(ProblemFeed, self).get_queryset() - + if self.have_editorial: queryset = queryset.filter(has_public_editorial=1) user = self.request.profile - if self.feed_type == 'new': - return queryset.order_by('-date') - elif user and self.feed_type == 'volunteer': - voted_problems = user.volunteer_problem_votes.values_list('problem', flat=True) - return queryset.exclude(id__in=voted_problems).order_by('?') + if self.feed_type == "new": + return queryset.order_by("-date") + elif user and self.feed_type == "volunteer": + voted_problems = user.volunteer_problem_votes.values_list( + "problem", flat=True + ) + return queryset.exclude(id__in=voted_problems).order_by("?") if not settings.ML_OUTPUT_PATH or not user: - return queryset.order_by('?') - + return queryset.order_by("?") + # Logging log_data = { - 'user': self.request.user.username, - 'cf': { - 'dot': {}, - 'cosine': {}, - }, - 'cf_time': { - 'dot': {}, - 'cosine': {} + "user": self.request.user.username, + "cf": { + "dot": {}, + "cosine": {}, }, + "cf_time": {"dot": {}, "cosine": {}}, } - cf_model = CollabFilter('collab_filter', log_time=log_data['cf']) - cf_time_model = CollabFilter('collab_filter_time', log_time=log_data['cf_time']) + cf_model = CollabFilter("collab_filter", log_time=log_data["cf"]) + cf_time_model = CollabFilter("collab_filter_time", log_time=log_data["cf_time"]) hot_problems_recommendations = [ - problem for problem in hot_problems(timedelta(days=7), 20) - if problem in queryset + problem + for problem in hot_problems(timedelta(days=7), 20) + if problem in queryset ] - q = self.merge_recommendation([ - cf_model.user_recommendations(user, queryset, cf_model.DOT, 100, - log_time=log_data['cf']['dot']), - cf_model.user_recommendations(user, queryset, cf_model.COSINE, 100, - log_time=log_data['cf']['cosine']), - cf_time_model.user_recommendations(user, queryset, cf_time_model.COSINE, 100, - log_time=log_data['cf_time']['cosine']), - cf_time_model.user_recommendations(user, queryset, cf_time_model.DOT, 100, - log_time=log_data['cf_time']['dot']), - hot_problems_recommendations - ]) + q = self.merge_recommendation( + [ + cf_model.user_recommendations( + user, queryset, cf_model.DOT, 100, log_time=log_data["cf"]["dot"] + ), + cf_model.user_recommendations( + user, + queryset, + cf_model.COSINE, + 100, + log_time=log_data["cf"]["cosine"], + ), + cf_time_model.user_recommendations( + user, + queryset, + cf_time_model.COSINE, + 100, + log_time=log_data["cf_time"]["cosine"], + ), + cf_time_model.user_recommendations( + user, + queryset, + cf_time_model.DOT, + 100, + log_time=log_data["cf_time"]["dot"], + ), + hot_problems_recommendations, + ] + ) cf_logger.info(log_data) return q def get_context_data(self, **kwargs): context = super(ProblemFeed, self).get_context_data(**kwargs) - context['page_type'] = 'feed' - context['title'] = self.title - context['feed_type'] = self.feed_type + context["page_type"] = "feed" + context["title"] = self.title + context["feed_type"] = self.feed_type return context def get(self, request, *args, **kwargs): if request.in_contest_mode: - return HttpResponseRedirect(reverse('problem_list')) + return HttpResponseRedirect(reverse("problem_list")) return super(ProblemFeed, self).get(request, *args, **kwargs) class LanguageTemplateAjax(View): def get(self, request, *args, **kwargs): try: - language = get_object_or_404(Language, id=int(request.GET.get('id', 0))) + language = get_object_or_404(Language, id=int(request.GET.get("id", 0))) except ValueError: raise Http404() - return HttpResponse(language.template, content_type='text/plain') + return HttpResponse(language.template, content_type="text/plain") class RandomProblem(ProblemList): @@ -714,48 +932,83 @@ class RandomProblem(ProblemList): queryset = self.get_normal_queryset() count = queryset.count() if not count: - return HttpResponseRedirect('%s%s%s' % (reverse('problem_list'), request.META['QUERY_STRING'] and '?', - request.META['QUERY_STRING'])) + return HttpResponseRedirect( + "%s%s%s" + % ( + reverse("problem_list"), + request.META["QUERY_STRING"] and "?", + request.META["QUERY_STRING"], + ) + ) return HttpResponseRedirect(queryset[randrange(count)].get_absolute_url()) -user_logger = logging.getLogger('judge.user') +user_logger = logging.getLogger("judge.user") @login_required def problem_submit(request, problem, submission=None): - if submission is not None and not request.user.has_perm('judge.resubmit_other') and \ - get_object_or_404(Submission, id=int(submission)).user.user != request.user: + if ( + submission is not None + and not request.user.has_perm("judge.resubmit_other") + and get_object_or_404(Submission, id=int(submission)).user.user != request.user + ): raise PermissionDenied() profile = request.profile problem = get_object_or_404(Problem, code=problem) if not problem.is_accessible_by(request.user): - if request.method == 'POST': - user_logger.info('Naughty user %s wants to submit to %s without permission', - request.user.username, problem.code) - return HttpResponseForbidden('

Not allowed to submit. Try later.

') + if request.method == "POST": + user_logger.info( + "Naughty user %s wants to submit to %s without permission", + request.user.username, + problem.code, + ) + return HttpResponseForbidden("

Not allowed to submit. Try later.

") raise Http404() if problem.is_editable_by(request.user): - judge_choices = tuple(Judge.objects.filter(online=True, problems=problem).values_list('name', 'name')) + judge_choices = tuple( + Judge.objects.filter(online=True, problems=problem).values_list( + "name", "name" + ) + ) else: judge_choices = () - if request.method == 'POST': - form = ProblemSubmitForm(request.POST, judge_choices=judge_choices, - instance=Submission(user=profile, problem=problem)) + if request.method == "POST": + form = ProblemSubmitForm( + request.POST, + judge_choices=judge_choices, + instance=Submission(user=profile, problem=problem), + ) if form.is_valid(): - if (not request.user.has_perm('judge.spam_submission') and - Submission.objects.filter(user=profile, was_rejudged=False) - .exclude(status__in=['D', 'IE', 'CE', 'AB']).count() >= settings.DMOJ_SUBMISSION_LIMIT): - return HttpResponse('

You submitted too many submissions.

', status=429) - if not problem.allowed_languages.filter(id=form.cleaned_data['language'].id).exists(): + if ( + not request.user.has_perm("judge.spam_submission") + and Submission.objects.filter(user=profile, was_rejudged=False) + .exclude(status__in=["D", "IE", "CE", "AB"]) + .count() + >= settings.DMOJ_SUBMISSION_LIMIT + ): + return HttpResponse( + "

You submitted too many submissions.

", status=429 + ) + if not problem.allowed_languages.filter( + id=form.cleaned_data["language"].id + ).exists(): raise PermissionDenied() - if not request.user.is_superuser and problem.banned_users.filter(id=profile.id).exists(): - return generic_message(request, _('Banned from submitting'), - _('You have been declared persona non grata for this problem. ' - 'You are permanently barred from submitting this problem.')) + if ( + not request.user.is_superuser + and problem.banned_users.filter(id=profile.id).exists() + ): + return generic_message( + request, + _("Banned from submitting"), + _( + "You have been declared persona non grata for this problem. " + "You are permanently barred from submitting this problem." + ), + ) with transaction.atomic(): if profile.current_contest is not None: @@ -766,51 +1019,72 @@ def problem_submit(request, problem, submission=None): model = form.save() else: max_subs = contest_problem.max_submissions - if max_subs and get_contest_submission_count(problem, profile, - profile.current_contest.virtual) >= max_subs: - return generic_message(request, _('Too many submissions'), - _('You have exceeded the submission limit for this problem.')) + if ( + max_subs + and get_contest_submission_count( + problem, profile, profile.current_contest.virtual + ) + >= max_subs + ): + return generic_message( + request, + _("Too many submissions"), + _( + "You have exceeded the submission limit for this problem." + ), + ) model = form.save() model.contest_object_id = contest_id - contest = ContestSubmission(submission=model, problem=contest_problem, - participation=profile.current_contest) + contest = ContestSubmission( + submission=model, + problem=contest_problem, + participation=profile.current_contest, + ) contest.save() else: model = form.save() # Create the SubmissionSource object - source = SubmissionSource(submission=model, source=form.cleaned_data['source']) + source = SubmissionSource( + submission=model, source=form.cleaned_data["source"] + ) source.save() profile.update_contest() # Save a query model.source = source - model.judge(rejudge=False, judge_id=form.cleaned_data['judge']) + model.judge(rejudge=False, judge_id=form.cleaned_data["judge"]) - return HttpResponseRedirect(reverse('submission_status', args=[str(model.id)])) + return HttpResponseRedirect( + reverse("submission_status", args=[str(model.id)]) + ) else: form_data = form.cleaned_data if submission is not None: sub = get_object_or_404(Submission, id=int(submission)) else: - initial = {'language': profile.language} + initial = {"language": profile.language} if submission is not None: try: - sub = get_object_or_404(Submission.objects.select_related('source', 'language'), id=int(submission)) - initial['source'] = sub.source.source - initial['language'] = sub.language + sub = get_object_or_404( + Submission.objects.select_related("source", "language"), + id=int(submission), + ) + initial["source"] = sub.source.source + initial["language"] = sub.language except ValueError: raise Http404() form = ProblemSubmitForm(judge_choices=judge_choices, initial=initial) form_data = initial - form.fields['language'].queryset = ( - problem.usable_languages.order_by('name', 'key') - .prefetch_related(Prefetch('runtimeversion_set', RuntimeVersion.objects.order_by('priority'))) + form.fields["language"].queryset = problem.usable_languages.order_by( + "name", "key" + ).prefetch_related( + Prefetch("runtimeversion_set", RuntimeVersion.objects.order_by("priority")) ) - if 'language' in form_data: - form.fields['source'].widget.mode = form_data['language'].ace - form.fields['source'].widget.theme = profile.ace_theme + if "language" in form_data: + form.fields["source"].widget.mode = form_data["language"].ace + form.fields["source"].widget.theme = profile.ace_theme if submission is not None: default_lang = sub.language @@ -820,37 +1094,52 @@ def problem_submit(request, problem, submission=None): submission_limit = submissions_left = None if profile.current_contest is not None: try: - submission_limit = problem.contests.get(contest=profile.current_contest.contest).max_submissions + submission_limit = problem.contests.get( + contest=profile.current_contest.contest + ).max_submissions except ContestProblem.DoesNotExist: pass else: if submission_limit: - submissions_left = submission_limit - get_contest_submission_count(problem, profile, - profile.current_contest.virtual) - return render(request, 'problem/submit.html', { - 'form': form, - 'title': _('Submit to %(problem)s') % { - 'problem': problem.translated_name(request.LANGUAGE_CODE), + submissions_left = submission_limit - get_contest_submission_count( + problem, profile, profile.current_contest.virtual + ) + return render( + request, + "problem/submit.html", + { + "form": form, + "title": _("Submit to %(problem)s") + % { + "problem": problem.translated_name(request.LANGUAGE_CODE), + }, + "content_title": mark_safe( + escape(_("Submit to %(problem)s")) + % { + "problem": format_html( + '{1}', + reverse("problem_detail", args=[problem.code]), + problem.translated_name(request.LANGUAGE_CODE), + ), + } + ), + "langs": Language.objects.all(), + "no_judges": not form.fields["language"].queryset, + "submission_limit": submission_limit, + "submissions_left": submissions_left, + "ACE_URL": settings.ACE_URL, + "default_lang": default_lang, }, - 'content_title': mark_safe(escape(_('Submit to %(problem)s')) % { - 'problem': format_html('{1}', - reverse('problem_detail', args=[problem.code]), - problem.translated_name(request.LANGUAGE_CODE)), - }), - 'langs': Language.objects.all(), - 'no_judges': not form.fields['language'].queryset, - 'submission_limit': submission_limit, - 'submissions_left': submissions_left, - 'ACE_URL': settings.ACE_URL, - 'default_lang': default_lang, - }) + ) -class ProblemClone(ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView): - title = _('Clone Problem') - template_name = 'problem/clone.html' +class ProblemClone( + ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView +): + title = _("Clone Problem") + template_name = "problem/clone.html" form_class = ProblemCloneForm - permission_required = 'judge.clone_problem' + permission_required = "judge.clone_problem" def form_valid(self, form): problem = self.object @@ -862,11 +1151,13 @@ class ProblemClone(ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObje problem.is_public = False problem.ac_rate = 0 problem.user_count = 0 - problem.code = form.cleaned_data['code'] + problem.code = form.cleaned_data["code"] problem.save() problem.authors.add(self.request.profile) problem.allowed_languages.set(languages) problem.language_limits.set(language_limits) problem.types.set(types) - return HttpResponseRedirect(reverse('admin:judge_problem_change', args=(problem.id,))) \ No newline at end of file + return HttpResponseRedirect( + reverse("admin:judge_problem_change", args=(problem.id,)) + ) diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py index eb38b84..0f0b38a 100644 --- a/judge/views/problem_data.py +++ b/judge/views/problem_data.py @@ -18,7 +18,15 @@ 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.forms import ( + BaseModelFormSet, + HiddenInput, + ModelForm, + NumberInput, + Select, + formset_factory, + FileInput, +) from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse @@ -28,46 +36,65 @@ from django.utils.translation import gettext as _ from django.views.generic import DetailView from judge.highlight_code import highlight_code -from judge.models import Problem, ProblemData, ProblemTestCase, Submission, problem_data_storage +from judge.models import ( + Problem, + ProblemData, + ProblemTestCase, + Submission, + problem_data_storage, +) 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.utils.fine_uploader import ( + combine_chunks, + save_upload, + handle_upload, + FineUploadFileInput, + FineUploadForm, +) from judge.views.problem import ProblemMixin mimetypes.init() -mimetypes.add_type('application/x-yaml', '.yml') +mimetypes.add_type("application/x-yaml", ".yml") def checker_args_cleaner(self): - data = self.cleaned_data['checker_args'] + data = self.cleaned_data["checker_args"] if not data or data.isspace(): - return '' + return "" try: if not isinstance(json.loads(data), dict): - raise ValidationError(_('Checker arguments must be a JSON object')) + raise ValidationError(_("Checker arguments must be a JSON object")) except ValueError: - raise ValidationError(_('Checker arguments is invalid JSON')) + raise ValidationError(_("Checker arguments is invalid JSON")) return data class ProblemDataForm(ModelForm): def clean_zipfile(self): - if hasattr(self, 'zip_valid') and not self.zip_valid: - raise ValidationError(_('Your zip file is invalid!')) - return self.cleaned_data['zipfile'] + if hasattr(self, "zip_valid") and not self.zip_valid: + raise ValidationError(_("Your zip file is invalid!")) + return self.cleaned_data["zipfile"] clean_checker_args = checker_args_cleaner class Meta: model = ProblemData - fields = ['zipfile', 'checker', 'checker_args', 'custom_checker', 'custom_validator', 'interactive_judge'] + fields = [ + "zipfile", + "checker", + "checker_args", + "custom_checker", + "custom_validator", + "interactive_judge", + ] widgets = { - 'zipfile': FineUploadFileInput, - 'checker_args': HiddenInput, - 'generator': HiddenInput, - 'output_limit': HiddenInput, - 'output_prefix': HiddenInput, + "zipfile": FineUploadFileInput, + "checker_args": HiddenInput, + "generator": HiddenInput, + "output_limit": HiddenInput, + "output_prefix": HiddenInput, } @@ -76,25 +103,35 @@ class ProblemCaseForm(ModelForm): class Meta: model = ProblemTestCase - fields = ('order', 'type', 'input_file', 'output_file', 'points', - 'is_pretest', 'checker', 'checker_args') #, 'output_limit', 'output_prefix', 'generator_args') + fields = ( + "order", + "type", + "input_file", + "output_file", + "points", + "is_pretest", + "checker", + "checker_args", + ) # , 'output_limit', 'output_prefix', 'generator_args') widgets = { # 'generator_args': HiddenInput, - 'type': Select(attrs={'style': 'width: 100%'}), - 'points': NumberInput(attrs={'style': 'width: 4em'}), + "type": Select(attrs={"style": "width: 100%"}), + "points": NumberInput(attrs={"style": "width: 4em"}), # 'output_prefix': NumberInput(attrs={'style': 'width: 4.5em'}), # 'output_limit': NumberInput(attrs={'style': 'width: 6em'}), # 'checker_args': HiddenInput, } - -class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, - can_delete=True)): +class ProblemCaseFormSet( + formset_factory( + ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, can_delete=True + ) +): model = ProblemTestCase def __init__(self, *args, **kwargs): - self.valid_files = kwargs.pop('valid_files', None) + self.valid_files = kwargs.pop("valid_files", None) super(ProblemCaseFormSet, self).__init__(*args, **kwargs) def _construct_form(self, i, **kwargs): @@ -114,14 +151,17 @@ class ProblemManagerMixin(LoginRequiredMixin, ProblemMixin, DetailView): class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView): - template_name = 'problem/submission-diff.html' + template_name = "problem/submission-diff.html" def get_title(self): - return _('Comparing submissions for {0}').format(self.object.name) + return _("Comparing submissions for {0}").format(self.object.name) def get_content_title(self): - return format_html(_('Comparing submissions for {0}'), self.object.name, - reverse('problem_detail', args=[self.object.code])) + return format_html( + _('Comparing submissions for {0}'), + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) def get_object(self, queryset=None): problem = super(ProblemSubmissionDiff, self).get_object(queryset) @@ -132,51 +172,67 @@ class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView): def get_context_data(self, **kwargs): context = super(ProblemSubmissionDiff, self).get_context_data(**kwargs) try: - ids = self.request.GET.getlist('id') + ids = self.request.GET.getlist("id") subs = Submission.objects.filter(id__in=ids) except ValueError: raise Http404 if not subs: raise Http404 - context['submissions'] = subs + context["submissions"] = subs # If we have associated data we can do better than just guess - data = ProblemTestCase.objects.filter(dataset=self.object, type='C') + data = ProblemTestCase.objects.filter(dataset=self.object, type="C") if data: num_cases = data.count() else: num_cases = subs.first().test_cases.count() - context['num_cases'] = num_cases + context["num_cases"] = num_cases return context class ProblemDataView(TitleMixin, ProblemManagerMixin): - template_name = 'problem/data.html' + template_name = "problem/data.html" def get_title(self): - return _('Editing data for {0}').format(self.object.name) + return _("Editing data for {0}").format(self.object.name) def get_content_title(self): - return mark_safe(escape(_('Editing data for %s')) % ( - format_html('{0}', self.object.name, - reverse('problem_detail', args=[self.object.code])))) + return mark_safe( + escape(_("Editing data for %s")) + % ( + format_html( + '{0}', + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) + ) + ) def get_data_form(self, post=False): - return ProblemDataForm(data=self.request.POST if post else None, prefix='problem-data', - files=self.request.FILES if post else None, - instance=ProblemData.objects.get_or_create(problem=self.object)[0]) + return ProblemDataForm( + data=self.request.POST if post else None, + prefix="problem-data", + files=self.request.FILES if post else None, + instance=ProblemData.objects.get_or_create(problem=self.object)[0], + ) def get_case_formset(self, files, post=False): - return ProblemCaseFormSet(data=self.request.POST if post else None, prefix='cases', valid_files=files, - queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by('order')) + return ProblemCaseFormSet( + data=self.request.POST if post else None, + prefix="cases", + valid_files=files, + queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by( + "order" + ), + ) def get_valid_files(self, data, post=False): try: - if post and 'problem-data-zipfile-clear' in self.request.POST: + if post and "problem-data-zipfile-clear" in self.request.POST: return [] - elif post and 'problem-data-zipfile' in self.request.FILES: - return ZipFile(self.request.FILES['problem-data-zipfile']).namelist() + elif post and "problem-data-zipfile" in self.request.FILES: + return ZipFile(self.request.FILES["problem-data-zipfile"]).namelist() elif data.zipfile: return ZipFile(data.zipfile.path).namelist() except BadZipfile: @@ -185,14 +241,18 @@ class ProblemDataView(TitleMixin, ProblemManagerMixin): def get_context_data(self, **kwargs): context = super(ProblemDataView, self).get_context_data(**kwargs) - if 'data_form' not in context: - context['data_form'] = self.get_data_form() - valid_files = context['valid_files'] = self.get_valid_files(context['data_form'].instance) - context['data_form'].zip_valid = valid_files is not False - context['cases_formset'] = self.get_case_formset(valid_files) - context['valid_files_json'] = mark_safe(json.dumps(context['valid_files'])) - context['valid_files'] = set(context['valid_files']) - context['all_case_forms'] = chain(context['cases_formset'], [context['cases_formset'].empty_form]) + if "data_form" not in context: + context["data_form"] = self.get_data_form() + valid_files = context["valid_files"] = self.get_valid_files( + context["data_form"].instance + ) + context["data_form"].zip_valid = valid_files is not False + context["cases_formset"] = self.get_case_formset(valid_files) + context["valid_files_json"] = mark_safe(json.dumps(context["valid_files"])) + context["valid_files"] = set(context["valid_files"]) + context["all_case_forms"] = chain( + context["cases_formset"], [context["cases_formset"].empty_form] + ) return context def post(self, request, *args, **kwargs): @@ -208,10 +268,17 @@ class ProblemDataView(TitleMixin, ProblemManagerMixin): case.save() for case in cases_formset.deleted_objects: case.delete() - ProblemDataCompiler.generate(problem, data, problem.cases.order_by('order'), valid_files) + ProblemDataCompiler.generate( + problem, data, problem.cases.order_by("order"), valid_files + ) return HttpResponseRedirect(request.get_full_path()) - return self.render_to_response(self.get_context_data(data_form=data_form, cases_formset=cases_formset, - valid_files=valid_files)) + return self.render_to_response( + self.get_context_data( + data_form=data_form, + cases_formset=cases_formset, + valid_files=valid_files, + ) + ) put = post @@ -223,16 +290,22 @@ def problem_data_file(request, problem, path): raise Http404() response = HttpResponse() - if hasattr(settings, 'DMOJ_PROBLEM_DATA_INTERNAL') and request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'): - response['X-Accel-Redirect'] = '%s/%s/%s' % (settings.DMOJ_PROBLEM_DATA_INTERNAL, problem, path) + if hasattr(settings, "DMOJ_PROBLEM_DATA_INTERNAL") and request.META.get( + "SERVER_SOFTWARE", "" + ).startswith("nginx/"): + response["X-Accel-Redirect"] = "%s/%s/%s" % ( + settings.DMOJ_PROBLEM_DATA_INTERNAL, + problem, + path, + ) else: try: - with problem_data_storage.open(os.path.join(problem, path), 'rb') as f: + with problem_data_storage.open(os.path.join(problem, path), "rb") as f: response.content = f.read() except IOError: raise Http404() - response['Content-Type'] = 'application/octet-stream' + response["Content-Type"] = "application/octet-stream" return response @@ -243,39 +316,53 @@ def problem_init_view(request, problem): raise Http404() try: - with problem_data_storage.open(os.path.join(problem.code, 'init.yml'), 'rb') as f: - data = utf8text(f.read()).rstrip('\n') + with problem_data_storage.open( + os.path.join(problem.code, "init.yml"), "rb" + ) as f: + data = utf8text(f.read()).rstrip("\n") except IOError: raise Http404() - return render(request, 'problem/yaml.html', { - 'raw_source': data, 'highlighted_source': highlight_code(data, 'yaml', linenos=False), - 'title': _('Generated init.yml for %s') % problem.name, - 'content_title': mark_safe(escape(_('Generated init.yml for %s')) % ( - format_html('{0}', problem.name, - reverse('problem_detail', args=[problem.code])))), - }) + return render( + request, + "problem/yaml.html", + { + "raw_source": data, + "highlighted_source": highlight_code(data, "yaml", linenos=False), + "title": _("Generated init.yml for %s") % problem.name, + "content_title": mark_safe( + escape(_("Generated init.yml for %s")) + % ( + format_html( + '{0}', + 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'] + 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: + 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() @@ -283,11 +370,16 @@ class ProblemZipUploadView(ProblemManagerMixin, View): raise Exception(e) finally: shutil.rmtree(dest) - + try: - handle_upload(request.FILES['qqfile'], form.cleaned_data, dest, post_upload=post_upload) + 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}) + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": True}) else: - return HttpResponse(status_code=400) \ No newline at end of file + return HttpResponse(status_code=400) diff --git a/judge/views/problem_manage.py b/judge/views/problem_manage.py index 07e94ec..5dd0aef 100644 --- a/judge/views/problem_manage.py +++ b/judge/views/problem_manage.py @@ -5,7 +5,12 @@ import zipfile, tempfile from celery.result import AsyncResult from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, +) from django.urls import reverse from django.utils.html import escape, format_html from django.utils.safestring import mark_safe @@ -46,33 +51,46 @@ class ManageProblemSubmissionActionMixin(ManageProblemSubmissionMixin): class ManageProblemSubmissionView(TitleMixin, ManageProblemSubmissionMixin, DetailView): - template_name = 'problem/manage_submission.html' + template_name = "problem/manage_submission.html" def get_title(self): - return _('Managing submissions for %s') % (self.object.name,) + return _("Managing submissions for %s") % (self.object.name,) def get_content_title(self): - return mark_safe(escape(_('Managing submissions for %s')) % ( - format_html('{0}', self.object.name, - reverse('problem_detail', args=[self.object.code])))) + return mark_safe( + escape(_("Managing submissions for %s")) + % ( + format_html( + '{0}', + self.object.name, + reverse("problem_detail", args=[self.object.code]), + ) + ) + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['submission_count'] = self.object.submission_set.count() - context['languages'] = [(lang_id, short_name or key) for lang_id, key, short_name in - Language.objects.values_list('id', 'key', 'short_name')] - context['results'] = sorted(map(itemgetter(0), Submission.RESULT)) + context["submission_count"] = self.object.submission_set.count() + context["languages"] = [ + (lang_id, short_name or key) + for lang_id, key, short_name in Language.objects.values_list( + "id", "key", "short_name" + ) + ] + context["results"] = sorted(map(itemgetter(0), Submission.RESULT)) return context -class BaseActionSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView): - permission_required = 'judge.rejudge_submission_lot' +class BaseActionSubmissionsView( + PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView +): + permission_required = "judge.rejudge_submission_lot" def perform_action(self): - if self.request.POST.get('use_range', 'off') == 'on': + if self.request.POST.get("use_range", "off") == "on": try: - start = int(self.request.POST.get('start')) - end = int(self.request.POST.get('end')) + start = int(self.request.POST.get("start")) + end = int(self.request.POST.get("end")) except (KeyError, ValueError): return HttpResponseBadRequest() id_range = (start, end) @@ -80,11 +98,13 @@ class BaseActionSubmissionsView(PermissionRequiredMixin, ManageProblemSubmission id_range = None try: - languages = list(map(int, self.request.POST.getlist('language'))) + languages = list(map(int, self.request.POST.getlist("language"))) except ValueError: return HttpResponseBadRequest() - return self.generate_response(id_range, languages, self.request.POST.getlist('result')) + return self.generate_response( + id_range, languages, self.request.POST.getlist("result") + ) def generate_response(self, id_range, languages, results): raise NotImplementedError() @@ -92,45 +112,64 @@ class BaseActionSubmissionsView(PermissionRequiredMixin, ManageProblemSubmission class ActionSubmissionsView(BaseActionSubmissionsView): def rejudge_response(self, id_range, languages, results): - status = rejudge_problem_filter.delay(self.object.id, id_range, languages, results) + status = rejudge_problem_filter.delay( + self.object.id, id_range, languages, results + ) return redirect_to_task_status( - status, message=_('Rejudging selected submissions for %s...') % (self.object.name,), - redirect=reverse('problem_submissions_rejudge_success', args=[self.object.code, status.id]), + status, + message=_("Rejudging selected submissions for %s...") % (self.object.name,), + redirect=reverse( + "problem_submissions_rejudge_success", + args=[self.object.code, status.id], + ), ) def download_response(self, id_range, languages, results): if not self.request.user.is_superuser: raise Http404 - + queryset = Submission.objects.filter(problem_id=self.object.id) submissions = apply_submission_filter(queryset, id_range, languages, results) with tempfile.SpooledTemporaryFile() as tmp: - with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as archive: + with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as archive: for submission in submissions: - file_name = str(submission.id) + '_' + str(submission.user) + '.' + str(submission.language.key) + file_name = ( + str(submission.id) + + "_" + + str(submission.user) + + "." + + str(submission.language.key) + ) archive.writestr(file_name, submission.source.source) - + # Reset file pointer tmp.seek(0) # Write file data to response - response = HttpResponse(tmp.read(), content_type='application/x-zip-compressed') - response['Content-Disposition'] = 'attachment; filename="%s"' % (str(self.object.code) + '_submissions.zip') + response = HttpResponse( + tmp.read(), content_type="application/x-zip-compressed" + ) + response["Content-Disposition"] = 'attachment; filename="%s"' % ( + str(self.object.code) + "_submissions.zip" + ) return response def generate_response(self, id_range, languages, results): - action = self.request.POST.get('action') - if (action == 'rejudge'): + action = self.request.POST.get("action") + if action == "rejudge": return self.rejudge_response(id_range, languages, results) - elif (action == 'download'): + elif action == "download": return self.download_response(id_range, languages, results) else: return Http404() + class PreviewActionSubmissionsView(BaseActionSubmissionsView): def generate_response(self, id_range, languages, results): - queryset = apply_submission_filter(self.object.submission_set.all(), id_range, languages, results) + queryset = apply_submission_filter( + self.object.submission_set.all(), id_range, languages, results + ) return HttpResponse(str(queryset.count())) @@ -138,8 +177,12 @@ class RescoreAllSubmissionsView(ManageProblemSubmissionActionMixin, BaseDetailVi def perform_action(self): status = rescore_problem.delay(self.object.id) return redirect_to_task_status( - status, message=_('Rescoring all submissions for %s...') % (self.object.name,), - redirect=reverse('problem_submissions_rescore_success', args=[self.object.code, status.id]), + status, + message=_("Rescoring all submissions for %s...") % (self.object.name,), + redirect=reverse( + "problem_submissions_rescore_success", + args=[self.object.code, status.id], + ), ) @@ -147,15 +190,29 @@ def rejudge_success(request, problem, task_id): count = AsyncResult(task_id).result if not isinstance(count, int): raise Http404() - messages.success(request, ngettext('Successfully scheduled %d submission for rejudging.', - 'Successfully scheduled %d submissions for rejudging.', count) % (count,)) - return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem])) + messages.success( + request, + ngettext( + "Successfully scheduled %d submission for rejudging.", + "Successfully scheduled %d submissions for rejudging.", + count, + ) + % (count,), + ) + return HttpResponseRedirect(reverse("problem_manage_submissions", args=[problem])) def rescore_success(request, problem, task_id): count = AsyncResult(task_id).result if not isinstance(count, int): raise Http404() - messages.success(request, ngettext('%d submission were successfully rescored.', - '%d submissions were successfully rescored.', count) % (count,)) - return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem])) + messages.success( + request, + ngettext( + "%d submission were successfully rescored.", + "%d submissions were successfully rescored.", + count, + ) + % (count,), + ) + return HttpResponseRedirect(reverse("problem_manage_submissions", args=[problem])) diff --git a/judge/views/ranked_submission.py b/judge/views/ranked_submission.py index f368d41..8038ac1 100644 --- a/judge/views/ranked_submission.py +++ b/judge/views/ranked_submission.py @@ -7,27 +7,31 @@ from judge.utils.problems import get_result_data from judge.utils.raw_sql import join_sql_subquery from judge.views.submission import ForceContestMixin, ProblemSubmissions -__all__ = ['RankedSubmissions', 'ContestRankedSubmission'] +__all__ = ["RankedSubmissions", "ContestRankedSubmission"] class RankedSubmissions(ProblemSubmissions): - tab = 'best_submissions_list' + tab = "best_submissions_list" dynamic_update = False def get_queryset(self): if self.in_contest: - contest_join = '''INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id) - INNER JOIN judge_contestparticipation AS cp ON (cs.participation_id = cp.id)''' - points = 'cs.points' - constraint = 'AND cp.contest_id = %s' + contest_join = """INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id) + INNER JOIN judge_contestparticipation AS cp ON (cs.participation_id = cp.id)""" + points = "cs.points" + constraint = "AND cp.contest_id = %s" else: - contest_join = '' - points = 'sub.points' - constraint = '' - queryset = super(RankedSubmissions, self).get_queryset().filter(user__is_unlisted=False) + contest_join = "" + points = "sub.points" + constraint = "" + queryset = ( + super(RankedSubmissions, self) + .get_queryset() + .filter(user__is_unlisted=False) + ) join_sql_subquery( queryset, - subquery=''' + subquery=""" SELECT sub.id AS id FROM ( SELECT sub.user_id AS uid, MAX(sub.points) AS points @@ -44,22 +48,30 @@ class RankedSubmissions(ProblemSubmissions): ON (sub.user_id = fastest.uid AND sub.time = fastest.time) {contest_join} WHERE sub.problem_id = %s AND {points} > 0 {constraint} GROUP BY sub.user_id - '''.format(points=points, contest_join=contest_join, constraint=constraint), - params=[self.problem.id, self.contest.id] * 3 if self.in_contest else [self.problem.id] * 3, - alias='best_subs', join_fields=[('id', 'id')], + """.format( + points=points, contest_join=contest_join, constraint=constraint + ), + params=[self.problem.id, self.contest.id] * 3 + if self.in_contest + else [self.problem.id] * 3, + alias="best_subs", + join_fields=[("id", "id")], ) if self.in_contest: - return queryset.order_by('-contest__points', 'time') + return queryset.order_by("-contest__points", "time") else: - return queryset.order_by('-points', 'time') + return queryset.order_by("-points", "time") def get_title(self): - return _('Best solutions for %s') % self.problem_name + return _("Best solutions for %s") % self.problem_name def get_content_title(self): - return format_html(_('Best solutions for {0}'), self.problem_name, - reverse('problem_detail', args=[self.problem.code])) + return format_html( + _('Best solutions for {0}'), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) def _get_result_data(self): return get_result_data(super(RankedSubmissions, self).get_queryset().order_by()) @@ -68,22 +80,35 @@ class RankedSubmissions(ProblemSubmissions): class ContestRankedSubmission(ForceContestMixin, RankedSubmissions): def get_title(self): if self.problem.is_accessible_by(self.request.user): - return _('Best solutions for %(problem)s in %(contest)s') % { - 'problem': self.problem_name, 'contest': self.contest.name, + return _("Best solutions for %(problem)s in %(contest)s") % { + "problem": self.problem_name, + "contest": self.contest.name, } - return _('Best solutions for problem %(number)s in %(contest)s') % { - 'number': self.get_problem_number(self.problem), 'contest': self.contest.name, + return _("Best solutions for problem %(number)s in %(contest)s") % { + "number": self.get_problem_number(self.problem), + "contest": self.contest.name, } def get_content_title(self): if self.problem.is_accessible_by(self.request.user): - return format_html(_('Best solutions for {0} in {2}'), - self.problem_name, reverse('problem_detail', args=[self.problem.code]), - self.contest.name, reverse('contest_view', args=[self.contest.key])) - return format_html(_('Best solutions for problem {0} in {1}'), - self.get_problem_number(self.problem), self.contest.name, - reverse('contest_view', args=[self.contest.key])) + return format_html( + _('Best solutions for {0} in {2}'), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) + return format_html( + _('Best solutions for problem {0} in {1}'), + self.get_problem_number(self.problem), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) def _get_result_data(self): - return get_result_data(Submission.objects.filter( - problem_id=self.problem.id, contest__participation__contest_id=self.contest.id)) + return get_result_data( + Submission.objects.filter( + problem_id=self.problem.id, + contest__participation__contest_id=self.contest.id, + ) + ) diff --git a/judge/views/register.py b/judge/views/register.py index e7ba8c9..0856f00 100644 --- a/judge/views/register.py +++ b/judge/views/register.py @@ -8,8 +8,10 @@ from django.contrib.auth.password_validation import get_default_password_validat from django.forms import ChoiceField, ModelChoiceField from django.shortcuts import render from django.utils.translation import gettext, gettext_lazy as _ -from registration.backends.default.views import (ActivationView as OldActivationView, - RegistrationView as OldRegistrationView) +from registration.backends.default.views import ( + ActivationView as OldActivationView, + RegistrationView as OldRegistrationView, +) from registration.forms import RegistrationForm from sortedm2m.forms import SortedMultipleChoiceField @@ -18,101 +20,138 @@ from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget from judge.utils.subscription import Subscription, newsletter_id from judge.widgets import Select2MultipleWidget, Select2Widget -valid_id = re.compile(r'^\w+$') +valid_id = re.compile(r"^\w+$") bad_mail_regex = list(map(re.compile, settings.BAD_MAIL_PROVIDER_REGEX)) class CustomRegistrationForm(RegistrationForm): - username = forms.RegexField(regex=r'^\w+$', max_length=30, label=_('Username'), - error_messages={'invalid': _('A username must contain letters, ' - 'numbers, or underscores')}) - timezone = ChoiceField(label=_('Timezone'), choices=TIMEZONE, - widget=Select2Widget(attrs={'style': 'width:100%'})) - language = ModelChoiceField(queryset=Language.objects.all(), label=_('Preferred language'), empty_label=None, - widget=Select2Widget(attrs={'style': 'width:100%'})) - organizations = SortedMultipleChoiceField(queryset=Organization.objects.filter(is_open=True), - label=_('Organizations'), required=False, - widget=Select2MultipleWidget(attrs={'style': 'width:100%'})) + username = forms.RegexField( + regex=r"^\w+$", + max_length=30, + label=_("Username"), + error_messages={ + "invalid": _("A username must contain letters, " "numbers, or underscores") + }, + ) + timezone = ChoiceField( + label=_("Timezone"), + choices=TIMEZONE, + widget=Select2Widget(attrs={"style": "width:100%"}), + ) + language = ModelChoiceField( + queryset=Language.objects.all(), + label=_("Preferred language"), + empty_label=None, + widget=Select2Widget(attrs={"style": "width:100%"}), + ) + organizations = SortedMultipleChoiceField( + queryset=Organization.objects.filter(is_open=True), + label=_("Organizations"), + required=False, + widget=Select2MultipleWidget(attrs={"style": "width:100%"}), + ) if newsletter_id is not None: - newsletter = forms.BooleanField(label=_('Subscribe to newsletter?'), initial=True, required=False) + newsletter = forms.BooleanField( + label=_("Subscribe to newsletter?"), initial=True, required=False + ) if ReCaptchaField is not None: captcha = ReCaptchaField(widget=ReCaptchaWidget()) def clean_organizations(self): - organizations = self.cleaned_data.get('organizations') or [] + 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)) + _( + "You may not be part of more than {count} public organizations." + ).format(count=max_orgs) + ) - return self.cleaned_data['organizations'] + 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 ' - 'is allowed per address.') % self.cleaned_data['email']) - if '@' in self.cleaned_data['email']: - domain = self.cleaned_data['email'].split('@')[-1].lower() - if (domain in settings.BAD_MAIL_PROVIDERS or - any(regex.match(domain) for regex in bad_mail_regex)): - raise forms.ValidationError(gettext('Your email provider is not allowed due to history of abuse. ' - 'Please use a reputable email provider.')) - return self.cleaned_data['email'] + if User.objects.filter(email=self.cleaned_data["email"]).exists(): + raise forms.ValidationError( + gettext( + 'The email address "%s" is already taken. Only one registration ' + "is allowed per address." + ) + % self.cleaned_data["email"] + ) + if "@" in self.cleaned_data["email"]: + domain = self.cleaned_data["email"].split("@")[-1].lower() + if domain in settings.BAD_MAIL_PROVIDERS or any( + regex.match(domain) for regex in bad_mail_regex + ): + raise forms.ValidationError( + gettext( + "Your email provider is not allowed due to history of abuse. " + "Please use a reputable email provider." + ) + ) + return self.cleaned_data["email"] class RegistrationView(OldRegistrationView): - title = _('Registration') + title = _("Registration") form_class = CustomRegistrationForm - template_name = 'registration/registration_form.html' + template_name = "registration/registration_form.html" def get_context_data(self, **kwargs): - if 'title' not in kwargs: - kwargs['title'] = self.title + if "title" not in kwargs: + kwargs["title"] = self.title tzmap = settings.TIMEZONE_MAP - kwargs['TIMEZONE_MAP'] = tzmap or 'http://momentjs.com/static/img/world.png' - kwargs['TIMEZONE_BG'] = settings.TIMEZONE_BG if tzmap else '#4E7CAD' - kwargs['password_validators'] = get_default_password_validators() - kwargs['tos_url'] = settings.TERMS_OF_SERVICE_URL + kwargs["TIMEZONE_MAP"] = tzmap or "http://momentjs.com/static/img/world.png" + kwargs["TIMEZONE_BG"] = settings.TIMEZONE_BG if tzmap else "#4E7CAD" + kwargs["password_validators"] = get_default_password_validators() + kwargs["tos_url"] = settings.TERMS_OF_SERVICE_URL return super(RegistrationView, self).get_context_data(**kwargs) def register(self, form): user = super(RegistrationView, self).register(form) - profile, _ = Profile.objects.get_or_create(user=user, defaults={ - 'language': Language.get_default_language(), - }) + profile, _ = Profile.objects.get_or_create( + user=user, + defaults={ + "language": Language.get_default_language(), + }, + ) cleaned_data = form.cleaned_data - profile.timezone = cleaned_data['timezone'] - profile.language = cleaned_data['language'] - profile.organizations.add(*cleaned_data['organizations']) + profile.timezone = cleaned_data["timezone"] + profile.language = cleaned_data["language"] + profile.organizations.add(*cleaned_data["organizations"]) profile.save() - if newsletter_id is not None and cleaned_data['newsletter']: + if newsletter_id is not None and cleaned_data["newsletter"]: Subscription(user=user, newsletter_id=newsletter_id, subscribed=True).save() return user def get_initial(self, *args, **kwargs): initial = super(RegistrationView, self).get_initial(*args, **kwargs) - initial['timezone'] = settings.DEFAULT_USER_TIME_ZONE - initial['language'] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE) + initial["timezone"] = settings.DEFAULT_USER_TIME_ZONE + initial["language"] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE) return initial class ActivationView(OldActivationView): - title = _('Registration') - template_name = 'registration/activate.html' + title = _("Registration") + template_name = "registration/activate.html" def get_context_data(self, **kwargs): - if 'title' not in kwargs: - kwargs['title'] = self.title + if "title" not in kwargs: + kwargs["title"] = self.title return super(ActivationView, self).get_context_data(**kwargs) def social_auth_error(request): - return render(request, 'generic-message.html', { - 'title': gettext('Authentication failure'), - 'message': request.GET.get('message'), - }) + return render( + request, + "generic-message.html", + { + "title": gettext("Authentication failure"), + "message": request.GET.get("message"), + }, + ) diff --git a/judge/views/select2.py b/judge/views/select2.py index 1a0bece..6068c5c 100644 --- a/judge/views/select2.py +++ b/judge/views/select2.py @@ -12,7 +12,7 @@ from judge.models import Comment, Contest, Organization, Problem, Profile def _get_user_queryset(term): qs = Profile.objects - if term.endswith(' '): + if term.endswith(" "): qs = qs.filter(user__username=term.strip()) else: qs = qs.filter(user__username__icontains=term) @@ -24,18 +24,22 @@ class Select2View(BaseListView): def get(self, request, *args, **kwargs): self.request = request - self.term = kwargs.get('term', request.GET.get('term', '')) + self.term = kwargs.get("term", request.GET.get("term", "")) self.object_list = self.get_queryset() context = self.get_context_data() - return JsonResponse({ - 'results': [ - { - 'text': smart_text(self.get_name(obj)), - 'id': obj.pk, - } for obj in context['object_list']], - 'more': context['page_obj'].has_next(), - }) + return JsonResponse( + { + "results": [ + { + "text": smart_text(self.get_name(obj)), + "id": obj.pk, + } + for obj in context["object_list"] + ], + "more": context["page_obj"].has_next(), + } + ) def get_name(self, obj): return str(obj) @@ -43,7 +47,11 @@ class Select2View(BaseListView): class UserSelect2View(Select2View): def get_queryset(self): - return _get_user_queryset(self.term).annotate(username=F('user__username')).only('id') + return ( + _get_user_queryset(self.term) + .annotate(username=F("user__username")) + .only("id") + ) def get_name(self, obj): return obj.username @@ -56,14 +64,18 @@ class OrganizationSelect2View(Select2View): class ProblemSelect2View(Select2View): def get_queryset(self): - return Problem.get_visible_problems(self.request.user) \ - .filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)).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): - return Contest.get_visible_contests(self.request.user) \ - .filter(Q(key__icontains=self.term) | Q(name__icontains=self.term)) + return Contest.get_visible_contests(self.request.user).filter( + Q(key__icontains=self.term) | Q(name__icontains=self.term) + ) class CommentSelect2View(Select2View): @@ -80,24 +92,32 @@ class UserSearchSelect2View(BaseListView): 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.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') + 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': username, - '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(), - }) + return JsonResponse( + { + "results": [ + { + "text": username, + "id": username, + "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) @@ -105,53 +125,66 @@ class UserSearchSelect2View(BaseListView): class ContestUserSearchSelect2View(UserSearchSelect2View): def get_queryset(self): - contest = get_object_or_404(Contest, key=self.kwargs['contest']) - if not contest.is_accessible_by(self.request.user) or not contest.can_see_full_scoreboard(self.request.user): + contest = get_object_or_404(Contest, key=self.kwargs["contest"]) + 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, - user__username__icontains=self.term).distinct() + return Profile.objects.filter( + contest_history__contest=contest, user__username__icontains=self.term + ).distinct() class TicketUserSelect2View(UserSearchSelect2View): def get_queryset(self): - return Profile.objects.filter(tickets__isnull=False, - user__username__icontains=self.term).distinct() + return Profile.objects.filter( + tickets__isnull=False, user__username__icontains=self.term + ).distinct() class AssigneeSelect2View(UserSearchSelect2View): def get_queryset(self): - return Profile.objects.filter(assigned_tickets__isnull=False, - user__username__icontains=self.term).distinct() + 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 + 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.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') + 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(), - }) + 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) \ No newline at end of file + return str(obj) diff --git a/judge/views/stats.py b/judge/views/stats.py index 41a852b..e8c74ef 100644 --- a/judge/views/stats.py +++ b/judge/views/stats.py @@ -9,31 +9,50 @@ from django.shortcuts import render from django.utils.translation import gettext as _ from judge.models import Language, Submission -from judge.utils.stats import chart_colors, get_bar_chart, get_pie_chart, highlight_colors +from judge.utils.stats import ( + chart_colors, + get_bar_chart, + get_pie_chart, + highlight_colors, +) -ac_count = Count(Case(When(submission__result='AC', then=Value(1)), output_field=IntegerField())) +ac_count = Count( + Case(When(submission__result="AC", then=Value(1)), output_field=IntegerField()) +) def repeat_chain(iterable): return chain.from_iterable(repeat(iterable)) -def language_data(request, language_count=Language.objects.annotate(count=Count('submission'))): - languages = language_count.filter(count__gt=0).values('key', 'name', 'count').order_by('-count') +def language_data( + request, language_count=Language.objects.annotate(count=Count("submission")) +): + languages = ( + language_count.filter(count__gt=0) + .values("key", "name", "count") + .order_by("-count") + ) num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD) - other_count = sum(map(itemgetter('count'), languages[num_languages:])) + other_count = sum(map(itemgetter("count"), languages[num_languages:])) - return JsonResponse({ - 'labels': list(map(itemgetter('name'), languages[:num_languages])) + ['Other'], - 'datasets': [ - { - 'backgroundColor': chart_colors[:num_languages] + ['#FDB45C'], - 'highlightBackgroundColor': highlight_colors[:num_languages] + ['#FFC870'], - 'data': list(map(itemgetter('count'), languages[:num_languages])) + [other_count], - }, - ], - }, safe=False) + return JsonResponse( + { + "labels": list(map(itemgetter("name"), languages[:num_languages])) + + ["Other"], + "datasets": [ + { + "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"], + "highlightBackgroundColor": highlight_colors[:num_languages] + + ["#FFC870"], + "data": list(map(itemgetter("count"), languages[:num_languages])) + + [other_count], + }, + ], + }, + safe=False, + ) def ac_language_data(request): @@ -42,27 +61,42 @@ def ac_language_data(request): def status_data(request, statuses=None): if not statuses: - statuses = (Submission.objects.values('result').annotate(count=Count('result')) - .values('result', 'count').order_by('-count')) + statuses = ( + Submission.objects.values("result") + .annotate(count=Count("result")) + .values("result", "count") + .order_by("-count") + ) data = [] for status in statuses: - res = status['result'] + res = status["result"] if not res: continue - count = status['count'] + count = status["count"] data.append((str(Submission.USER_DISPLAY_CODES[res]), count)) return JsonResponse(get_pie_chart(data), safe=False) def ac_rate(request): - rate = CombinedExpression(ac_count / Count('submission'), '*', Value(100.0), output_field=FloatField()) - data = Language.objects.annotate(total=Count('submission'), ac_rate=rate).filter(total__gt=0) \ - .order_by('total').values_list('name', 'ac_rate') + rate = CombinedExpression( + ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField() + ) + data = ( + Language.objects.annotate(total=Count("submission"), ac_rate=rate) + .filter(total__gt=0) + .order_by("total") + .values_list("name", "ac_rate") + ) return JsonResponse(get_bar_chart(list(data))) def language(request): - return render(request, 'stats/language.html', { - 'title': _('Language statistics'), 'tab': 'language', - }) + return render( + request, + "stats/language.html", + { + "title": _("Language statistics"), + "tab": "language", + }, + ) diff --git a/judge/views/status.py b/judge/views/status.py index a7f0b26..25902ec 100644 --- a/judge/views/status.py +++ b/judge/views/status.py @@ -8,35 +8,43 @@ from packaging import version from judge.models import Judge, Language, RuntimeVersion -__all__ = ['status_all', 'status_table'] +__all__ = ["status_all", "status_table"] def get_judges(request): if request.user.is_superuser or request.user.is_staff: - return True, Judge.objects.order_by('-online', 'name') + return True, Judge.objects.order_by("-online", "name") else: return False, Judge.objects.filter(online=True) def status_all(request): see_all, judges = get_judges(request) - return render(request, 'status/judge-status.html', { - 'title': _('Status'), - 'judges': judges, - 'see_all_judges': see_all, - }) + return render( + request, + "status/judge-status.html", + { + "title": _("Status"), + "judges": judges, + "see_all_judges": see_all, + }, + ) def status_table(request): see_all, judges = get_judges(request) - return render(request, 'status/judge-status-table.html', { - 'judges': judges, - 'see_all_judges': see_all, - }) + return render( + request, + "status/judge-status-table.html", + { + "judges": judges, + "see_all_judges": see_all, + }, + ) class LatestList(list): - __slots__ = ('versions', 'is_latest') + __slots__ = ("versions", "is_latest") def compare_version_list(x, y): @@ -61,11 +69,13 @@ def version_matrix(request): judges = {judge.id: judge.name for judge in Judge.objects.filter(online=True)} languages = Language.objects.filter(judges__online=True).distinct() - for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by('priority'): + for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by( + "priority" + ): matrix[runtime.judge_id][runtime.language_id].append(runtime) for judge, data in six.iteritems(matrix): - name_tuple = judges[judge].rpartition('.') + name_tuple = judges[judge].rpartition(".") groups[name_tuple[0] or name_tuple[-1]].append((judges[judge], data)) matrix = {} @@ -103,9 +113,13 @@ def version_matrix(request): versions.is_latest = versions.versions == latest[language] languages = sorted(languages, key=lambda lang: version.parse(lang.name)) - return render(request, 'status/versions.html', { - 'title': _('Version matrix'), - 'judges': sorted(matrix.keys()), - 'languages': languages, - 'matrix': matrix, - }) + return render( + request, + "status/versions.html", + { + "title": _("Version matrix"), + "judges": sorted(matrix.keys()), + "languages": languages, + "matrix": matrix, + }, + ) diff --git a/judge/views/submission.py b/judge/views/submission.py index d10da79..42d941c 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -50,16 +50,33 @@ from judge.utils.views import TitleMixin def submission_related(queryset): - return queryset.select_related('user__user', 'problem', 'language') \ - .only('id', 'user__user__username', 'user__display_rank', 'user__rating', 'problem__name', - 'problem__code', 'problem__is_public', 'language__short_name', 'language__key', 'date', 'time', 'memory', - 'points', 'result', 'status', 'case_points', 'case_total', 'current_testcase', 'contest_object') + return queryset.select_related("user__user", "problem", "language").only( + "id", + "user__user__username", + "user__display_rank", + "user__rating", + "problem__name", + "problem__code", + "problem__is_public", + "language__short_name", + "language__key", + "date", + "time", + "memory", + "points", + "result", + "status", + "case_points", + "case_total", + "current_testcase", + "contest_object", + ) class SubmissionMixin(object): model = Submission - context_object_name = 'submission' - pk_url_kwarg = 'submission' + context_object_name = "submission" + pk_url_kwarg = "submission" class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView): @@ -67,63 +84,76 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta submission = super(SubmissionDetailBase, self).get_object(queryset) profile = self.request.profile problem = submission.problem - if self.request.user.has_perm('judge.view_all_submission'): + if self.request.user.has_perm("judge.view_all_submission"): return submission if submission.user_id == profile.id: return submission if problem.is_editor(profile): return submission if problem.is_public or problem.testers.filter(id=profile.id).exists(): - if Submission.objects.filter(user_id=profile.id, result='AC', problem_id=problem.id, - points=problem.points).exists(): + if Submission.objects.filter( + user_id=profile.id, + result="AC", + problem_id=problem.id, + points=problem.points, + ).exists(): return submission - if (hasattr(submission, 'contest') and - submission.contest.participation.contest.is_editable_by(self.request.user)): + if hasattr( + submission, "contest" + ) and submission.contest.participation.contest.is_editable_by( + self.request.user + ): return submission - + raise PermissionDenied() def get_title(self): submission = self.object - return _('Submission of %(problem)s by %(user)s') % { - 'problem': submission.problem.translated_name(self.request.LANGUAGE_CODE), - 'user': submission.user.user.username, + return _("Submission of %(problem)s by %(user)s") % { + "problem": submission.problem.translated_name(self.request.LANGUAGE_CODE), + "user": submission.user.user.username, } def get_content_title(self): submission = self.object - return mark_safe(escape(_('Submission of %(problem)s by %(user)s')) % { - 'problem': format_html('{1}', - reverse('problem_detail', args=[ - submission.problem.code]), - submission.problem.translated_name(self.request.LANGUAGE_CODE)), - 'user': format_html('{1}', - reverse('user_page', args=[ - submission.user.user.username]), - submission.user.user.username), - }) + return mark_safe( + escape(_("Submission of %(problem)s by %(user)s")) + % { + "problem": format_html( + '{1}', + reverse("problem_detail", args=[submission.problem.code]), + submission.problem.translated_name(self.request.LANGUAGE_CODE), + ), + "user": format_html( + '{1}', + reverse("user_page", args=[submission.user.user.username]), + submission.user.user.username, + ), + } + ) class SubmissionSource(SubmissionDetailBase): - template_name = 'submission/source.html' + template_name = "submission/source.html" def get_queryset(self): - return super().get_queryset().select_related('source') + return super().get_queryset().select_related("source") def get_context_data(self, **kwargs): context = super(SubmissionSource, self).get_context_data(**kwargs) submission = self.object - context['raw_source'] = submission.source.source.rstrip('\n') - context['highlighted_source'] = highlight_code( - submission.source.source, submission.language.pygments, linenos=False) + context["raw_source"] = submission.source.source.rstrip("\n") + context["highlighted_source"] = highlight_code( + submission.source.source, submission.language.pygments, linenos=False + ) return context def make_batch(batch, cases): - result = {'id': batch, 'cases': cases} + result = {"id": batch, "cases": cases} if batch: - result['points'] = min(map(attrgetter('points'), cases)) - result['total'] = max(map(attrgetter('total'), cases)) + result["points"] = min(map(attrgetter("points"), cases)) + result["total"] = max(map(attrgetter("total"), cases)) return result @@ -143,33 +173,37 @@ def group_test_cases(cases): def get_cases_data(submission): - testcases = ProblemTestCase.objects.filter(dataset=submission.problem)\ - .order_by('order') - - if (submission.is_pretested): + 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) + 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 = {} count = 0 for case in testcases: - if case.type != 'C': continue + 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 '', + "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 class SubmissionStatus(SubmissionDetailBase): - template_name = 'submission/status.html' + template_name = "submission/status.html" def access_testcases_in_contest(self): contest = self.object.contest_or_none @@ -186,46 +220,48 @@ class SubmissionStatus(SubmissionDetailBase): 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 - + 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): + if contest is not None: prefix_length = contest.problem.output_prefix_override - + if contest is None or prefix_length > 0 or can_see_testcases: - context['cases_data'] = get_cases_data(submission) - context['can_see_testcases'] = True + context["cases_data"] = get_cases_data(submission) + context["can_see_testcases"] = True try: lang_limit = submission.problem.language_limits.get( - language=submission.language) + language=submission.language + ) except ObjectDoesNotExist: pass else: - context['time_limit'] = lang_limit.time_limit + context["time_limit"] = lang_limit.time_limit return context class SubmissionTestCaseQuery(SubmissionStatus): - template_name = 'submission/status-testcases.html' + template_name = "submission/status-testcases.html" def get(self, request, *args, **kwargs): - if 'id' not in request.GET or not request.GET['id'].isdigit(): + if "id" not in request.GET or not request.GET["id"].isdigit(): return HttpResponseBadRequest() self.kwargs[self.pk_url_kwarg] = kwargs[self.pk_url_kwarg] = int( - request.GET['id']) + request.GET["id"] + ) return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs) class SubmissionSourceRaw(SubmissionSource): def get(self, request, *args, **kwargs): submission = self.get_object() - return HttpResponse(submission.source.source, content_type='text/plain') + return HttpResponse(submission.source.source, content_type="text/plain") @require_POST @@ -234,28 +270,29 @@ def abort_submission(request, submission): # if (not request.user.is_authenticated or (submission.was_rejudged or (request.profile != submission.user)) and # not request.user.has_perm('abort_any_submission')): # raise PermissionDenied() - if (not request.user.is_authenticated - or not request.user.has_perm('abort_any_submission')): + if not request.user.is_authenticated or not request.user.has_perm( + "abort_any_submission" + ): raise PermissionDenied() submission.abort() - return HttpResponseRedirect(reverse('submission_status', args=(submission.id,))) + return HttpResponseRedirect(reverse("submission_status", args=(submission.id,))) class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): model = Submission paginate_by = 50 show_problem = True - title = gettext_lazy('All submissions') - content_title = gettext_lazy('All submissions') - tab = 'all_submissions_list' - template_name = 'submission/list.html' - context_object_name = 'submissions' + title = gettext_lazy("All submissions") + content_title = gettext_lazy("All submissions") + tab = "all_submissions_list" + template_name = "submission/list.html" + context_object_name = "submissions" first_page_href = None def get_result_data(self): result = self._get_result_data() - for category in result['categories']: - category['name'] = _(category['name']) + for category in result["categories"]: + category["name"] = _(category["name"]) return result def _get_result_data(self): @@ -266,8 +303,11 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): @cached_property def in_contest(self): - return self.request.user.is_authenticated and self.request.profile.current_contest is not None \ + return ( + self.request.user.is_authenticated + and self.request.profile.current_contest is not None and self.request.in_contest_mode + ) @cached_property def contest(self): @@ -276,34 +316,46 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): def _get_queryset(self): queryset = Submission.objects.all() use_straight_join(queryset) - queryset = submission_related(queryset.order_by('-id')) + queryset = submission_related(queryset.order_by("-id")) if self.show_problem: - queryset = queryset.prefetch_related(Prefetch('problem__translations', - queryset=ProblemTranslation.objects.filter( - language=self.request.LANGUAGE_CODE), to_attr='_trans')) + queryset = queryset.prefetch_related( + Prefetch( + "problem__translations", + queryset=ProblemTranslation.objects.filter( + language=self.request.LANGUAGE_CODE + ), + to_attr="_trans", + ) + ) if self.in_contest: 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') + queryset = queryset.select_related("contest_object").defer( + "contest_object__description" + ) # 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'): + if not self.request.user.has_perm("judge.see_private_contest"): # 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)) + 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__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) @@ -314,10 +366,15 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): if not self.in_contest: join_sql_subquery( queryset, - subquery=str(Problem.get_visible_problems(self.request.user).distinct().only('id').query), + subquery=str( + Problem.get_visible_problems(self.request.user) + .distinct() + .only("id") + .query + ), params=[], - join_fields=[('problem_id', 'id')], - alias='visible_problems', + join_fields=[("problem_id", "id")], + alias="visible_problems", ) return queryset @@ -325,43 +382,49 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): return None def get_all_submissions_page(self): - return reverse('all_submissions') + return reverse("all_submissions") def get_searchable_status_codes(self): - hidden_codes = ['SC'] + hidden_codes = ["SC"] if not self.request.user.is_superuser and not self.request.user.is_staff: - hidden_codes += ['IE'] - return [(key, value) for key, value in Submission.RESULT if key not in hidden_codes] + hidden_codes += ["IE"] + return [ + (key, value) for key, value in Submission.RESULT if key not in hidden_codes + ] def get_context_data(self, **kwargs): context = super(SubmissionsListBase, self).get_context_data(**kwargs) authenticated = self.request.user.is_authenticated - context['dynamic_update'] = False - context['show_problem'] = self.show_problem - context['completed_problem_ids'] = user_completed_ids( - self.request.profile) if authenticated else [] - context['authored_problem_ids'] = user_authored_ids( - self.request.profile) if authenticated else [] - context['editable_problem_ids'] = user_editable_ids( - self.request.profile) if authenticated else [] + context["dynamic_update"] = False + context["show_problem"] = self.show_problem + context["completed_problem_ids"] = ( + user_completed_ids(self.request.profile) if authenticated else [] + ) + context["authored_problem_ids"] = ( + user_authored_ids(self.request.profile) if authenticated else [] + ) + context["editable_problem_ids"] = ( + user_editable_ids(self.request.profile) if authenticated else [] + ) - context['all_languages'] = Language.objects.all( - ).values_list('key', 'name') - context['selected_languages'] = self.selected_languages + context["all_languages"] = Language.objects.all().values_list("key", "name") + context["selected_languages"] = self.selected_languages - context['all_statuses'] = self.get_searchable_status_codes() - context['selected_statuses'] = self.selected_statuses + context["all_statuses"] = self.get_searchable_status_codes() + context["selected_statuses"] = self.selected_statuses - context['results_json'] = mark_safe(json.dumps(self.get_result_data())) - context['results_colors_json'] = mark_safe( - json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS)) + context["results_json"] = mark_safe(json.dumps(self.get_result_data())) + context["results_colors_json"] = mark_safe( + json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS) + ) - context['page_suffix'] = suffix = ( - '?' + self.request.GET.urlencode()) if self.request.GET else '' - context['first_page_href'] = (self.first_page_href or '.') + suffix - context['my_submissions_link'] = self.get_my_submissions_page() - context['all_submissions_link'] = self.get_all_submissions_page() - context['tab'] = self.tab + context["page_suffix"] = suffix = ( + ("?" + self.request.GET.urlencode()) if self.request.GET else "" + ) + context["first_page_href"] = (self.first_page_href or ".") + suffix + context["my_submissions_link"] = self.get_my_submissions_page() + context["all_submissions_link"] = self.get_all_submissions_page() + context["tab"] = self.tab return context @@ -370,10 +433,10 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): if check is not None: return check - self.selected_languages = set(request.GET.getlist('language')) - self.selected_statuses = set(request.GET.getlist('status')) + self.selected_languages = set(request.GET.getlist("language")) + self.selected_statuses = set(request.GET.getlist("status")) - if 'results' in request.GET: + if "results" in request.GET: return JsonResponse(self.get_result_data()) return super(SubmissionsListBase, self).get(request, *args, **kwargs) @@ -381,50 +444,57 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): class UserMixin(object): def get(self, request, *args, **kwargs): - if 'user' not in kwargs: - raise ImproperlyConfigured('Must pass a user') - self.profile = get_object_or_404( - Profile, user__username=kwargs['user']) - self.username = kwargs['user'] + if "user" not in kwargs: + raise ImproperlyConfigured("Must pass a user") + self.profile = get_object_or_404(Profile, user__username=kwargs["user"]) + self.username = kwargs["user"] return super(UserMixin, self).get(request, *args, **kwargs) class ConditionalUserTabMixin(object): def get_context_data(self, **kwargs): - context = super(ConditionalUserTabMixin, - self).get_context_data(**kwargs) + context = super(ConditionalUserTabMixin, self).get_context_data(**kwargs) if self.request.user.is_authenticated and self.request.profile == self.profile: - context['tab'] = 'my_submissions_tab' + context["tab"] = "my_submissions_tab" else: - context['tab'] = 'user_submissions_tab' - context['tab_username'] = self.profile.user.username + context["tab"] = "user_submissions_tab" + context["tab_username"] = self.profile.user.username return context class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase): def get_queryset(self): - return super(AllUserSubmissions, self).get_queryset().filter(user_id=self.profile.id) + return ( + super(AllUserSubmissions, self) + .get_queryset() + .filter(user_id=self.profile.id) + ) def get_title(self): if self.request.user.is_authenticated and self.request.profile == self.profile: - return _('All my submissions') - return _('All submissions by %s') % self.username + return _("All my submissions") + return _("All submissions by %s") % self.username def get_content_title(self): if self.request.user.is_authenticated and self.request.profile == self.profile: - return format_html('All my submissions') - return format_html('All submissions by {0}', self.username, - reverse('user_page', args=[self.username])) + return format_html("All my submissions") + return format_html( + 'All submissions by {0}', + self.username, + reverse("user_page", args=[self.username]), + ) def get_my_submissions_page(self): if self.request.user.is_authenticated: - return reverse('all_user_submissions', kwargs={'user': self.request.user.username}) + return reverse( + "all_user_submissions", kwargs={"user": self.request.user.username} + ) def get_context_data(self, **kwargs): context = super(AllUserSubmissions, self).get_context_data(**kwargs) - context['dynamic_update'] = context['page_obj'].number == 1 - context['dynamic_user_id'] = self.profile.id - context['last_msg'] = event.last() + context["dynamic_update"] = context["page_obj"].number == 1 + context["dynamic_user_id"] = self.profile.id + context["last_msg"] = event.last() return context @@ -434,16 +504,28 @@ class ProblemSubmissionsBase(SubmissionsListBase): check_contest_in_access_check = True def get_queryset(self): - if self.in_contest and not self.contest.contest_problems.filter(problem_id=self.problem.id).exists(): + if ( + self.in_contest + and not self.contest.contest_problems.filter( + problem_id=self.problem.id + ).exists() + ): raise Http404() - return super(ProblemSubmissionsBase, self)._get_queryset().filter(problem_id=self.problem.id) + return ( + super(ProblemSubmissionsBase, self) + ._get_queryset() + .filter(problem_id=self.problem.id) + ) def get_title(self): - return _('All submissions for %s') % self.problem_name + return _("All submissions for %s") % self.problem_name def get_content_title(self): - return format_html('All submissions for {0}', self.problem_name, - reverse('problem_detail', args=[self.problem.code])) + return format_html( + 'All submissions for {0}', + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) def access_check_contest(self, request): if self.in_contest and not self.contest.can_see_own_scoreboard(request.user): @@ -457,33 +539,39 @@ class ProblemSubmissionsBase(SubmissionsListBase): self.access_check_contest(request) def get(self, request, *args, **kwargs): - if 'problem' not in kwargs: - raise ImproperlyConfigured(_('Must pass a problem')) - self.problem = get_object_or_404(Problem, code=kwargs['problem']) - self.problem_name = self.problem.translated_name( - self.request.LANGUAGE_CODE) + if "problem" not in kwargs: + raise ImproperlyConfigured(_("Must pass a problem")) + self.problem = get_object_or_404(Problem, code=kwargs["problem"]) + self.problem_name = self.problem.translated_name(self.request.LANGUAGE_CODE) return super(ProblemSubmissionsBase, self).get(request, *args, **kwargs) def get_all_submissions_page(self): - return reverse('chronological_submissions', kwargs={'problem': self.problem.code}) + return reverse( + "chronological_submissions", kwargs={"problem": self.problem.code} + ) def get_context_data(self, **kwargs): - context = super(ProblemSubmissionsBase, - self).get_context_data(**kwargs) + context = super(ProblemSubmissionsBase, self).get_context_data(**kwargs) if self.dynamic_update: - context['dynamic_update'] = context['page_obj'].number == 1 - context['dynamic_problem_id'] = self.problem.id - context['last_msg'] = event.last() - context['best_submissions_link'] = reverse('ranked_submissions', kwargs={ - 'problem': self.problem.code}) + context["dynamic_update"] = context["page_obj"].number == 1 + context["dynamic_problem_id"] = self.problem.id + context["last_msg"] = event.last() + context["best_submissions_link"] = reverse( + "ranked_submissions", kwargs={"problem": self.problem.code} + ) return context class ProblemSubmissions(ProblemSubmissionsBase): def get_my_submissions_page(self): if self.request.user.is_authenticated: - return reverse('user_submissions', kwargs={'problem': self.problem.code, - 'user': self.request.user.username}) + return reverse( + "user_submissions", + kwargs={ + "problem": self.problem.code, + "user": self.request.user.username, + }, + ) class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissions): @@ -491,7 +579,9 @@ class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissi @cached_property def is_own(self): - return self.request.user.is_authenticated and self.request.profile == self.profile + return ( + self.request.user.is_authenticated and self.request.profile == self.profile + ) def access_check(self, request): super(UserProblemSubmissions, self).access_check(request) @@ -500,60 +590,84 @@ class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissi self.access_check_contest(request) def get_queryset(self): - return super(UserProblemSubmissions, self).get_queryset().filter(user_id=self.profile.id) + return ( + super(UserProblemSubmissions, self) + .get_queryset() + .filter(user_id=self.profile.id) + ) def get_title(self): if self.is_own: - return _("My submissions for %(problem)s") % {'problem': self.problem_name} - return _("%(user)s's submissions for %(problem)s") % {'user': self.username, 'problem': self.problem_name} + return _("My submissions for %(problem)s") % {"problem": self.problem_name} + return _("%(user)s's submissions for %(problem)s") % { + "user": self.username, + "problem": self.problem_name, + } def get_content_title(self): if self.request.user.is_authenticated and self.request.profile == self.profile: - return format_html('''My submissions for {2}''', - self.username, reverse( - 'user_page', args=[self.username]), - self.problem_name, reverse('problem_detail', args=[self.problem.code])) - return format_html('''{0}'s submissions for {2}''', - self.username, reverse( - 'user_page', args=[self.username]), - self.problem_name, reverse('problem_detail', args=[self.problem.code])) + return format_html( + """My submissions for {2}""", + self.username, + reverse("user_page", args=[self.username]), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) + return format_html( + """{0}'s submissions for {2}""", + self.username, + reverse("user_page", args=[self.username]), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + ) def get_context_data(self, **kwargs): - context = super(UserProblemSubmissions, - self).get_context_data(**kwargs) - context['dynamic_user_id'] = self.profile.id + context = super(UserProblemSubmissions, self).get_context_data(**kwargs) + context["dynamic_user_id"] = self.profile.id return context def single_submission(request, submission_id, show_problem=True): request.no_profile_update = True authenticated = request.user.is_authenticated - submission = get_object_or_404(submission_related( - Submission.objects.all()), id=int(submission_id)) + submission = get_object_or_404( + submission_related(Submission.objects.all()), id=int(submission_id) + ) if not submission.problem.is_accessible_by(request.user): raise Http404() - return render(request, 'submission/row.html', { - 'submission': submission, - 'authored_problem_ids': user_authored_ids(request.profile) if authenticated else [], - 'completed_problem_ids': user_completed_ids(request.profile) if authenticated else [], - 'editable_problem_ids': user_editable_ids(request.profile) if authenticated else [], - 'show_problem': show_problem, - 'problem_name': show_problem and submission.problem.translated_name(request.LANGUAGE_CODE), - 'profile_id': request.profile.id if authenticated else 0, - }) + return render( + request, + "submission/row.html", + { + "submission": submission, + "authored_problem_ids": user_authored_ids(request.profile) + if authenticated + else [], + "completed_problem_ids": user_completed_ids(request.profile) + if authenticated + else [], + "editable_problem_ids": user_editable_ids(request.profile) + if authenticated + else [], + "show_problem": show_problem, + "problem_name": show_problem + and submission.problem.translated_name(request.LANGUAGE_CODE), + "profile_id": request.profile.id if authenticated else 0, + }, + ) def single_submission_query(request): request.no_profile_update = True - if 'id' not in request.GET or not request.GET['id'].isdigit(): + if "id" not in request.GET or not request.GET["id"].isdigit(): return HttpResponseBadRequest() try: - show_problem = int(request.GET.get('show_problem', '1')) + show_problem = int(request.GET.get("show_problem", "1")) except ValueError: return HttpResponseBadRequest() - return single_submission(request, int(request.GET['id']), bool(show_problem)) + return single_submission(request, int(request.GET["id"]), bool(show_problem)) class AllSubmissions(SubmissionsListBase): @@ -561,20 +675,22 @@ class AllSubmissions(SubmissionsListBase): def get_my_submissions_page(self): if self.request.user.is_authenticated: - return reverse('all_user_submissions', kwargs={'user': self.request.user.username}) + return reverse( + "all_user_submissions", kwargs={"user": self.request.user.username} + ) def get_context_data(self, **kwargs): context = super(AllSubmissions, self).get_context_data(**kwargs) - context['dynamic_update'] = context['page_obj'].number == 1 - context['last_msg'] = event.last() - context['stats_update_interval'] = self.stats_update_interval + context["dynamic_update"] = context["page_obj"].number == 1 + context["last_msg"] = event.last() + context["stats_update_interval"] = self.stats_update_interval return context def _get_result_data(self): if self.in_contest or self.selected_languages or self.selected_statuses: return super(AllSubmissions, self)._get_result_data() - key = 'global_submission_result_data' + key = "global_submission_result_data" result = cache.get(key) if result: return result @@ -595,28 +711,42 @@ class ForceContestMixin(object): def access_check(self, request): super(ForceContestMixin, self).access_check(request) - if not request.user.has_perm('judge.see_private_contest'): + if not request.user.has_perm("judge.see_private_contest"): if not self.contest.is_visible: raise Http404() - if self.contest.start_time is not None and self.contest.start_time > timezone.now(): + if ( + self.contest.start_time is not None + and self.contest.start_time > timezone.now() + ): raise Http404() def get_problem_number(self, problem): - return self.contest.contest_problems.select_related('problem').get(problem=problem).order + return ( + self.contest.contest_problems.select_related("problem") + .get(problem=problem) + .order + ) def get(self, request, *args, **kwargs): - if 'contest' not in kwargs: - raise ImproperlyConfigured(_('Must pass a contest')) - self._contest = get_object_or_404(Contest, key=kwargs['contest']) + if "contest" not in kwargs: + raise ImproperlyConfigured(_("Must pass a contest")) + self._contest = get_object_or_404(Contest, key=kwargs["contest"]) return super(ForceContestMixin, self).get(request, *args, **kwargs) class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): def get_title(self): if self.problem.is_accessible_by(self.request.user): - return "%s's submissions for %s in %s" % (self.username, self.problem_name, self.contest.name) + return "%s's submissions for %s in %s" % ( + self.username, + self.problem_name, + self.contest.name, + ) return "%s's submissions for problem %s in %s" % ( - self.username, self.get_problem_number(self.problem), self.contest.name) + self.username, + self.get_problem_number(self.problem), + self.contest.name, + ) def access_check(self, request): super(UserContestSubmissions, self).access_check(request) @@ -625,16 +755,26 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): def get_content_title(self): if self.problem.is_accessible_by(self.request.user): - return format_html(_('{0}\'s submissions for ' - '{2} in {4}'), - self.username, reverse( - 'user_page', args=[self.username]), - self.problem_name, reverse( - 'problem_detail', args=[self.problem.code]), - self.contest.name, reverse('contest_view', args=[self.contest.key])) - return format_html(_('{0}\'s submissions for ' - 'problem {2} in {3}'), - self.username, reverse( - 'user_page', args=[self.username]), - self.get_problem_number(self.problem), - self.contest.name, reverse('contest_view', args=[self.contest.key])) + return format_html( + _( + '{0}\'s submissions for ' + '{2} in {4}' + ), + self.username, + reverse("user_page", args=[self.username]), + self.problem_name, + reverse("problem_detail", args=[self.problem.code]), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) + return format_html( + _( + '{0}\'s submissions for ' + 'problem {2} in {3}' + ), + self.username, + reverse("user_page", args=[self.username]), + self.get_problem_number(self.problem), + self.contest.name, + reverse("contest_view", args=[self.contest.key]), + ) diff --git a/judge/views/tasks.py b/judge/views/tasks.py index 42e123f..a1c0644 100644 --- a/judge/views/tasks.py +++ b/judge/views/tasks.py @@ -4,7 +4,12 @@ from uuid import UUID from celery.result import AsyncResult from django.core.exceptions import PermissionDenied -from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse +from django.http import ( + Http404, + HttpResponseBadRequest, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import render from django.urls import reverse from django.utils.http import is_safe_url @@ -17,14 +22,19 @@ from judge.utils.views import short_circuit_middleware def get_task_status(task_id): result = AsyncResult(task_id) info = result.result - if result.state == 'PROGRESS': - return {'code': 'PROGRESS', 'done': info['done'], 'total': info['total'], 'stage': info['stage']} - elif result.state == 'SUCCESS': - return {'code': 'SUCCESS'} - elif result.state == 'FAILURE': - return {'code': 'FAILURE', 'error': str(info)} + if result.state == "PROGRESS": + return { + "code": "PROGRESS", + "done": info["done"], + "total": info["total"], + "stage": info["stage"], + } + elif result.state == "SUCCESS": + return {"code": "SUCCESS"} + elif result.state == "FAILURE": + return {"code": "FAILURE", "error": str(info)} else: - return {'code': 'WORKING'} + return {"code": "WORKING"} def task_status(request, task_id): @@ -33,34 +43,48 @@ def task_status(request, task_id): except ValueError: raise Http404() - redirect = request.GET.get('redirect') + redirect = request.GET.get("redirect") if not is_safe_url(redirect, allowed_hosts={request.get_host()}): redirect = None status = get_task_status(task_id) - if status['code'] == 'SUCCESS' and redirect: + if status["code"] == "SUCCESS" and redirect: return HttpResponseRedirect(redirect) - return render(request, 'task_status.html', { - 'task_id': task_id, 'task_status': json.dumps(status), - 'message': request.GET.get('message', ''), 'redirect': redirect or '', - }) + return render( + request, + "task_status.html", + { + "task_id": task_id, + "task_status": json.dumps(status), + "message": request.GET.get("message", ""), + "redirect": redirect or "", + }, + ) @short_circuit_middleware def task_status_ajax(request): - if 'id' not in request.GET: - return HttpResponseBadRequest('Need to pass GET parameter "id"', content_type='text/plain') - return JsonResponse(get_task_status(request.GET['id'])) + if "id" not in request.GET: + return HttpResponseBadRequest( + 'Need to pass GET parameter "id"', content_type="text/plain" + ) + return JsonResponse(get_task_status(request.GET["id"])) def demo_task(request, task, message): if not request.user.is_superuser: raise PermissionDenied() result = task.delay() - return redirect_to_task_status(result, message=message, redirect=reverse('home')) + return redirect_to_task_status(result, message=message, redirect=reverse("home")) -demo_success = partial(demo_task, task=success, message='Running example task that succeeds...') -demo_failure = partial(demo_task, task=failure, message='Running example task that fails...') -demo_progress = partial(demo_task, task=progress, message='Running example task that waits 10 seconds...') +demo_success = partial( + demo_task, task=success, message="Running example task that succeeds..." +) +demo_failure = partial( + demo_task, task=failure, message="Running example task that fails..." +) +demo_progress = partial( + demo_task, task=progress, message="Running example task that waits 10 seconds..." +) diff --git a/judge/views/ticket.py b/judge/views/ticket.py index afde514..3d62235 100644 --- a/judge/views/ticket.py +++ b/judge/views/ticket.py @@ -4,8 +4,18 @@ from itertools import chain from django import forms from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import ImproperlyConfigured, PermissionDenied, ValidationError -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse +from django.core.exceptions import ( + ImproperlyConfigured, + PermissionDenied, + ValidationError, +) +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import get_object_or_404 from django.template.defaultfilters import truncatechars from django.template.loader import get_template @@ -26,90 +36,107 @@ from judge.utils.views import SingleObjectFormView, TitleMixin, paginate_query_c from judge.views.problem import ProblemMixin from judge.widgets import HeavyPreviewPageDownWidget -ticket_widget = (forms.Textarea() if HeavyPreviewPageDownWidget is None else - HeavyPreviewPageDownWidget(preview=reverse_lazy('ticket_preview'), - preview_timeout=1000, hide_preview_button=True)) +ticket_widget = ( + forms.Textarea() + if HeavyPreviewPageDownWidget is None + else HeavyPreviewPageDownWidget( + preview=reverse_lazy("ticket_preview"), + preview_timeout=1000, + hide_preview_button=True, + ) +) def add_ticket_notifications(users, author, link, ticket): - html = f"{ticket.linked_item}" - + html = f'{ticket.linked_item}' + users = set(users) if author in users: users.remove(author) for user in users: - notification = Notification(owner=user, - html_link=html, - category='Ticket', - author=author) + notification = Notification( + owner=user, html_link=html, category="Ticket", author=author + ) notification.save() class TicketForm(forms.Form): - title = forms.CharField(max_length=100, label=gettext_lazy('Ticket title')) + title = forms.CharField(max_length=100, label=gettext_lazy("Ticket title")) body = forms.CharField(widget=ticket_widget) def __init__(self, request, *args, **kwargs): self.request = request super(TicketForm, self).__init__(*args, **kwargs) - self.fields['title'].widget.attrs.update({'placeholder': _('Ticket title')}) - self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')}) + self.fields["title"].widget.attrs.update({"placeholder": _("Ticket title")}) + self.fields["body"].widget.attrs.update({"placeholder": _("Issue description")}) def clean(self): if self.request is not None and self.request.user.is_authenticated: profile = self.request.profile if profile.mute: - raise ValidationError(_('Your part is silent, little toad.')) + raise ValidationError(_("Your part is silent, little toad.")) return super(TicketForm, self).clean() class NewTicketView(LoginRequiredMixin, SingleObjectFormView): form_class = TicketForm - template_name = 'ticket/new.html' + template_name = "ticket/new.html" def get_assignees(self): return [] def get_form_kwargs(self): kwargs = super(NewTicketView, self).get_form_kwargs() - kwargs['request'] = self.request + kwargs["request"] = self.request return kwargs def form_valid(self, form): - ticket = Ticket(user=self.request.profile, title=form.cleaned_data['title']) + ticket = Ticket(user=self.request.profile, title=form.cleaned_data["title"]) ticket.linked_item = self.object ticket.save() - message = TicketMessage(ticket=ticket, user=ticket.user, body=form.cleaned_data['body']) + message = TicketMessage( + ticket=ticket, user=ticket.user, body=form.cleaned_data["body"] + ) message.save() ticket.assignees.set(self.get_assignees()) - link = reverse('ticket', args=[ticket.id]) + link = reverse("ticket", args=[ticket.id]) add_ticket_notifications(ticket.assignees.all(), ticket.user, link, ticket) if event.real: - event.post('tickets', { - 'type': 'new-ticket', 'id': ticket.id, - 'message': message.id, 'user': ticket.user_id, - 'assignees': list(ticket.assignees.values_list('id', flat=True)), - }) + event.post( + "tickets", + { + "type": "new-ticket", + "id": ticket.id, + "message": message.id, + "user": ticket.user_id, + "assignees": list(ticket.assignees.values_list("id", flat=True)), + }, + ) return HttpResponseRedirect(link) class NewProblemTicketView(ProblemMixin, TitleMixin, NewTicketView): - template_name = 'ticket/new_problem.html' + template_name = "ticket/new_problem.html" def get_assignees(self): return self.object.authors.all() def get_title(self): - return _('New ticket for %s') % self.object.name + return _("New ticket for %s") % self.object.name def get_content_title(self): - return mark_safe(escape(_('New ticket for %s')) % - format_html('{1}', reverse('problem_detail', args=[self.object.code]), - self.object.translated_name(self.request.LANGUAGE_CODE))) + return mark_safe( + escape(_("New ticket for %s")) + % format_html( + '{1}', + reverse("problem_detail", args=[self.object.code]), + self.object.translated_name(self.request.LANGUAGE_CODE), + ) + ) def form_valid(self, form): if not self.object.is_accessible_by(self.request.user): @@ -127,7 +154,7 @@ class TicketMixin(object): def get_object(self, queryset=None): ticket = super(TicketMixin, self).get_object(queryset) profile_id = self.request.profile.id - if self.request.user.has_perm('judge.change_ticket'): + if self.request.user.has_perm("judge.change_ticket"): return ticket if ticket.user_id == profile_id: return ticket @@ -141,39 +168,55 @@ class TicketMixin(object): class TicketView(TitleMixin, LoginRequiredMixin, TicketMixin, SingleObjectFormView): form_class = TicketCommentForm - template_name = 'ticket/ticket.html' - context_object_name = 'ticket' + template_name = "ticket/ticket.html" + context_object_name = "ticket" def form_valid(self, form): - message = TicketMessage(user=self.request.profile, - body=form.cleaned_data['body'], - ticket=self.object) + message = TicketMessage( + user=self.request.profile, + body=form.cleaned_data["body"], + ticket=self.object, + ) message.save() - link = '%s#message-%d' % (reverse('ticket', args=[self.object.id]), message.id) + link = "%s#message-%d" % (reverse("ticket", args=[self.object.id]), message.id) notify_list = list(chain(self.object.assignees.all(), [self.object.user])) add_ticket_notifications(notify_list, message.user, link, self.object) if event.real: - event.post('tickets', { - 'type': 'ticket-message', 'id': self.object.id, - 'message': message.id, 'user': self.object.user_id, - 'assignees': list(self.object.assignees.values_list('id', flat=True)), - }) - event.post('ticket-%d' % self.object.id, { - 'type': 'ticket-message', 'message': message.id, - }) + event.post( + "tickets", + { + "type": "ticket-message", + "id": self.object.id, + "message": message.id, + "user": self.object.user_id, + "assignees": list( + self.object.assignees.values_list("id", flat=True) + ), + }, + ) + event.post( + "ticket-%d" % self.object.id, + { + "type": "ticket-message", + "message": message.id, + }, + ) return HttpResponseRedirect(link) def get_title(self): - return _('%(title)s - Ticket %(id)d') % {'title': self.object.title, 'id': self.object.id} + return _("%(title)s - Ticket %(id)d") % { + "title": self.object.title, + "id": self.object.id, + } def get_context_data(self, **kwargs): context = super(TicketView, self).get_context_data(**kwargs) - context['ticket_messages'] = self.object.messages.select_related('user__user') - context['assignees'] = self.object.assignees.select_related('user') - context['last_msg'] = event.last() + context["ticket_messages"] = self.object.messages.select_related("user__user") + context["assignees"] = self.object.assignees.select_related("user") + context["last_msg"] = event.last() return context @@ -182,21 +225,32 @@ class TicketStatusChangeView(LoginRequiredMixin, TicketMixin, SingleObjectMixin, def post(self, request, *args, **kwargs): if self.open is None: - raise ImproperlyConfigured('Need to define open') + raise ImproperlyConfigured("Need to define open") ticket = self.get_object() if ticket.is_open != self.open: ticket.is_open = self.open ticket.save() if event.real: - event.post('tickets', { - 'type': 'ticket-status', 'id': ticket.id, - 'open': self.open, 'user': ticket.user_id, - 'assignees': list(ticket.assignees.values_list('id', flat=True)), - 'title': ticket.title, - }) - event.post('ticket-%d' % ticket.id, { - 'type': 'ticket-status', 'open': self.open, - }) + event.post( + "tickets", + { + "type": "ticket-status", + "id": ticket.id, + "open": self.open, + "user": ticket.user_id, + "assignees": list( + ticket.assignees.values_list("id", flat=True) + ), + "title": ticket.title, + }, + ) + event.post( + "ticket-%d" % ticket.id, + { + "type": "ticket-status", + "open": self.open, + }, + ) return HttpResponse(status=204) @@ -205,16 +259,16 @@ class TicketNotesForm(forms.Form): class TicketNotesEditView(LoginRequiredMixin, TicketMixin, SingleObjectFormView): - template_name = 'ticket/edit-notes.html' + template_name = "ticket/edit-notes.html" form_class = TicketNotesForm - context_object_name = 'ticket' + context_object_name = "ticket" def get_initial(self): - return {'notes': self.get_object().notes} + return {"notes": self.get_object().notes} def form_valid(self, form): ticket = self.get_object() - ticket.notes = notes = form.cleaned_data['notes'] + ticket.notes = notes = form.cleaned_data["notes"] ticket.save() if notes: return HttpResponse(linebreaks(notes, autoescape=True)) @@ -227,8 +281,8 @@ class TicketNotesEditView(LoginRequiredMixin, TicketMixin, SingleObjectFormView) class TicketList(LoginRequiredMixin, ListView): model = Ticket - template_name = 'ticket/list.html' - context_object_name = 'tickets' + template_name = "ticket/list.html" + context_object_name = "tickets" paginate_by = 50 paginator_class = DiggPaginator @@ -242,32 +296,38 @@ class TicketList(LoginRequiredMixin, ListView): @cached_property def can_edit_all(self): - return self.request.user.has_perm('judge.change_ticket') + return self.request.user.has_perm("judge.change_ticket") @cached_property def filter_users(self): - return self.request.GET.getlist('user') + return self.request.GET.getlist("user") @cached_property def filter_assignees(self): - return self.request.GET.getlist('assignee') + return self.request.GET.getlist("assignee") def GET_with_session(self, key): if not self.request.GET: return self.request.session.get(key, False) - return self.request.GET.get(key, None) == '1' + return self.request.GET.get(key, None) == "1" def _get_queryset(self): - return Ticket.objects.select_related('user__user').prefetch_related('assignees__user').order_by('-id') + return ( + Ticket.objects.select_related("user__user") + .prefetch_related("assignees__user") + .order_by("-id") + ) def get_queryset(self): queryset = self._get_queryset() - if self.GET_with_session('own'): + if self.GET_with_session("own"): queryset = queryset.filter(own_ticket_filter(self.profile.id)) elif not self.can_edit_all: queryset = filter_visible_tickets(queryset, self.user, self.profile) if self.filter_assignees: - queryset = queryset.filter(assignees__user__username__in=self.filter_assignees) + queryset = queryset.filter( + assignees__user__username__in=self.filter_assignees + ) if self.filter_users: queryset = queryset.filter(user__user__username__in=self.filter_users) return queryset.distinct() @@ -275,29 +335,41 @@ class TicketList(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super(TicketList, self).get_context_data(**kwargs) - page = context['page_obj'] - context['title'] = _('Tickets - Page %(number)d of %(total)d') % { - 'number': page.number, - 'total': page.paginator.num_pages, + page = context["page_obj"] + context["title"] = _("Tickets - Page %(number)d of %(total)d") % { + "number": page.number, + "total": page.paginator.num_pages, } - context['can_edit_all'] = self.can_edit_all - context['filter_status'] = { - 'own': self.GET_with_session('own'), 'user': self.filter_users, 'assignee': self.filter_assignees, - 'user_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_users) - .values_list('id', flat=True))), - 'assignee_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_assignees) - .values_list('id', flat=True))), - 'own_id': self.profile.id if self.GET_with_session('own') else 'null', + context["can_edit_all"] = self.can_edit_all + context["filter_status"] = { + "own": self.GET_with_session("own"), + "user": self.filter_users, + "assignee": self.filter_assignees, + "user_id": json.dumps( + list( + Profile.objects.filter( + user__username__in=self.filter_users + ).values_list("id", flat=True) + ) + ), + "assignee_id": json.dumps( + list( + Profile.objects.filter( + user__username__in=self.filter_assignees + ).values_list("id", flat=True) + ) + ), + "own_id": self.profile.id if self.GET_with_session("own") else "null", } - context['last_msg'] = event.last() + context["last_msg"] = event.last() context.update(paginate_query_context(self.request)) return context def post(self, request, *args, **kwargs): - to_update = ('own',) + to_update = ("own",) for key in to_update: if key in request.GET: - val = request.GET.get(key) == '1' + val = request.GET.get(key) == "1" request.session[key] = val else: request.session.pop(key, None) @@ -306,38 +378,56 @@ class TicketList(LoginRequiredMixin, ListView): class ProblemTicketListView(TicketList): def _get_queryset(self): - problem = get_object_or_404(Problem, code=self.kwargs.get('problem')) + problem = get_object_or_404(Problem, code=self.kwargs.get("problem")) if problem.is_editable_by(self.request.user): - return problem.tickets.order_by('-id') + return problem.tickets.order_by("-id") elif problem.is_accessible_by(self.request.user): - return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by('-id') + return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by( + "-id" + ) raise Http404() class TicketListDataAjax(TicketMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): try: - self.kwargs['pk'] = request.GET['id'] + self.kwargs["pk"] = request.GET["id"] except KeyError: return HttpResponseBadRequest() ticket = self.get_object() message = ticket.messages.first() - return JsonResponse({ - 'row': get_template('ticket/row.html').render({'ticket': ticket}, request), - 'notification': { - 'title': _('New Ticket: %s') % ticket.title, - 'body': '%s\n%s' % (_('#%(id)d, assigned to: %(users)s') % { - 'id': ticket.id, - 'users': (_(', ').join(ticket.assignees.values_list('user__username', flat=True)) or _('no one')), - }, truncatechars(message.body, 200)), - }, - }) + return JsonResponse( + { + "row": get_template("ticket/row.html").render( + {"ticket": ticket}, request + ), + "notification": { + "title": _("New Ticket: %s") % ticket.title, + "body": "%s\n%s" + % ( + _("#%(id)d, assigned to: %(users)s") + % { + "id": ticket.id, + "users": ( + _(", ").join( + ticket.assignees.values_list( + "user__username", flat=True + ) + ) + or _("no one") + ), + }, + truncatechars(message.body, 200), + ), + }, + } + ) class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): try: - message_id = request.GET['message'] + message_id = request.GET["message"] except KeyError: return HttpResponseBadRequest() ticket = self.get_object() @@ -345,10 +435,14 @@ class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View): message = ticket.messages.get(id=message_id) except TicketMessage.DoesNotExist: return HttpResponseBadRequest() - return JsonResponse({ - 'message': get_template('ticket/message.html').render({'message': message}, request), - 'notification': { - 'title': _('New Ticket Message For: %s') % ticket.title, - 'body': truncatechars(message.body, 200), - }, - }) + return JsonResponse( + { + "message": get_template("ticket/message.html").render( + {"message": message}, request + ), + "notification": { + "title": _("New Ticket Message For: %s") % ticket.title, + "body": truncatechars(message.body, 200), + }, + } + ) diff --git a/judge/views/totp.py b/judge/views/totp.py index 097137f..88fd63a 100644 --- a/judge/views/totp.py +++ b/judge/views/totp.py @@ -21,7 +21,7 @@ class TOTPView(TitleMixin, LoginRequiredMixin, FormView): def get_form_kwargs(self): result = super(TOTPView, self).get_form_kwargs() - result['totp_key'] = self.profile.totp_key + result["totp_key"] = self.profile.totp_key return result def dispatch(self, request, *args, **kwargs): @@ -35,12 +35,12 @@ class TOTPView(TitleMixin, LoginRequiredMixin, FormView): raise NotImplementedError() def next_page(self): - return HttpResponseRedirect(reverse('user_edit_profile')) + return HttpResponseRedirect(reverse("user_edit_profile")) class TOTPEnableView(TOTPView): - title = _('Enable Two Factor Authentication') - template_name = 'registration/totp_enable.html' + title = _("Enable Two Factor Authentication") + template_name = "registration/totp_enable.html" def get(self, request, *args, **kwargs): profile = self.profile @@ -54,20 +54,22 @@ class TOTPEnableView(TOTPView): def post(self, request, *args, **kwargs): if not self.profile.totp_key: - return HttpResponseBadRequest('No TOTP key generated on server side?') + return HttpResponseBadRequest("No TOTP key generated on server side?") return super(TOTPEnableView, self).post(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(TOTPEnableView, self).get_context_data(**kwargs) - context['totp_key'] = self.profile.totp_key - context['qr_code'] = self.render_qr_code(self.request.user.username, self.profile.totp_key) + context["totp_key"] = self.profile.totp_key + context["qr_code"] = self.render_qr_code( + self.request.user.username, self.profile.totp_key + ) return context def form_valid(self, form): self.profile.is_totp_enabled = True self.profile.save() # Make sure users don't get prompted to enter code right after enabling: - self.request.session['2fa_passed'] = True + self.request.session["2fa_passed"] = True return self.next_page() @classmethod @@ -79,15 +81,17 @@ class TOTPEnableView(TOTPView): qr.add_data(uri) qr.make(fit=True) - image = qr.make_image(fill_color='black', back_color='white') + image = qr.make_image(fill_color="black", back_color="white") buf = BytesIO() - image.save(buf, format='PNG') - return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii') + image.save(buf, format="PNG") + return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode( + "ascii" + ) class TOTPDisableView(TOTPView): - title = _('Disable Two Factor Authentication') - template_name = 'registration/totp_disable.html' + title = _("Disable Two Factor Authentication") + template_name = "registration/totp_disable.html" def check_skip(self): if not self.profile.is_totp_enabled: @@ -102,21 +106,25 @@ class TOTPDisableView(TOTPView): class TOTPLoginView(SuccessURLAllowedHostsMixin, TOTPView): - title = _('Perform Two Factor Authentication') - template_name = 'registration/totp_auth.html' + title = _("Perform Two Factor Authentication") + template_name = "registration/totp_auth.html" def check_skip(self): - return not self.profile.is_totp_enabled or self.request.session.get('2fa_passed', False) + return not self.profile.is_totp_enabled or self.request.session.get( + "2fa_passed", False + ) def next_page(self): - redirect_to = self.request.GET.get('next', '') + redirect_to = self.request.GET.get("next", "") url_is_safe = is_safe_url( url=redirect_to, allowed_hosts=self.get_success_url_allowed_hosts(), require_https=self.request.is_secure(), ) - return HttpResponseRedirect((redirect_to if url_is_safe else '') or reverse('user_page')) + return HttpResponseRedirect( + (redirect_to if url_is_safe else "") or reverse("user_page") + ) def form_valid(self, form): - self.request.session['2fa_passed'] = True + self.request.session["2fa_passed"] = True return self.next_page() diff --git a/judge/views/user.py b/judge/views/user.py index a14617a..0f06664 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -14,7 +14,14 @@ 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.forms import Form -from django.http import Http404, HttpResponseRedirect, JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponse +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 @@ -36,10 +43,16 @@ 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, SingleObjectFormView +from judge.utils.views import ( + DiggPaginatorMixin, + QueryStringSortMixin, + TitleMixin, + generic_message, + SingleObjectFormView, +) from .contests import ContestRanking -__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile'] +__all__ = ["UserPage", "UserAboutPage", "UserProblemsPage", "users", "edit_profile"] def remap_keys(iterable, mapping): @@ -48,16 +61,16 @@ def remap_keys(iterable, mapping): class UserMixin(object): model = Profile - slug_field = 'user__username' - slug_url_kwarg = 'user' - context_object_name = 'user' + slug_field = "user__username" + slug_url_kwarg = "user" + context_object_name = "user" def render_to_response(self, context, **response_kwargs): return super(UserMixin, self).render_to_response(context, **response_kwargs) class UserPage(TitleMixin, UserMixin, DetailView): - template_name = 'user/user-base.html' + template_name = "user/user-base.html" def get_object(self, queryset=None): if self.kwargs.get(self.slug_url_kwarg, None) is None: @@ -71,12 +84,18 @@ class UserPage(TitleMixin, UserMixin, DetailView): try: return super(UserPage, self).dispatch(request, *args, **kwargs) except Http404: - return generic_message(request, _('No such user'), _('No user handle "%s".') % - self.kwargs.get(self.slug_url_kwarg, None)) + return generic_message( + request, + _("No such user"), + _('No user handle "%s".') % self.kwargs.get(self.slug_url_kwarg, None), + ) def get_title(self): - return (_('My account') if self.request.user == self.object.user else - _('User %s') % self.object.user.username) + 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 @@ -92,8 +111,11 @@ class UserPage(TitleMixin, UserMixin, DetailView): @cached_property def in_contest(self): - return self.profile is not None and self.profile.current_contest is not None \ + return ( + self.profile is not None + and self.profile.current_contest is not None and self.request.in_contest_mode + ) def get_completed_problems(self): if self.in_contest: @@ -104,29 +126,49 @@ class UserPage(TitleMixin, UserMixin, DetailView): def get_context_data(self, **kwargs): context = super(UserPage, self).get_context_data(**kwargs) - context['followed'] = Friend.is_friend(self.request.profile, self.object) - context['hide_solved'] = int(self.hide_solved) - context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \ - .order_by('code') + context["followed"] = Friend.is_friend(self.request.profile, self.object) + context["hide_solved"] = int(self.hide_solved) + context["authored"] = self.object.authored_problems.filter( + is_public=True, is_organization_private=False + ).order_by("code") - rating = self.object.ratings.order_by('-contest__end_time')[:1] - context['rating'] = rating[0] if rating else None + rating = self.object.ratings.order_by("-contest__end_time")[:1] + context["rating"] = rating[0] if rating else None - context['rank'] = Profile.objects.filter( - is_unlisted=False, performance_points__gt=self.object.performance_points, - ).count() + 1 + context["rank"] = ( + Profile.objects.filter( + is_unlisted=False, + performance_points__gt=self.object.performance_points, + ).count() + + 1 + ) if rating: - context['rating_rank'] = Profile.objects.filter( - is_unlisted=False, rating__gt=self.object.rating, - ).count() + 1 - context['rated_users'] = Profile.objects.filter(is_unlisted=False, rating__isnull=False).count() - context.update(self.object.ratings.aggregate(min_rating=Min('rating'), max_rating=Max('rating'), - contests=Count('contest'))) + context["rating_rank"] = ( + Profile.objects.filter( + is_unlisted=False, + rating__gt=self.object.rating, + ).count() + + 1 + ) + context["rated_users"] = Profile.objects.filter( + is_unlisted=False, rating__isnull=False + ).count() + context.update( + self.object.ratings.aggregate( + min_rating=Min("rating"), + max_rating=Max("rating"), + contests=Count("contest"), + ) + ) return context def get(self, request, *args, **kwargs): - self.hide_solved = request.GET.get('hide_solved') == '1' if 'hide_solved' in request.GET else False + self.hide_solved = ( + request.GET.get("hide_solved") == "1" + if "hide_solved" in request.GET + else False + ) return super(UserPage, self).get(request, *args, **kwargs) @@ -134,20 +176,27 @@ EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) class UserAboutPage(UserPage): - template_name = 'user/user-about.html' + template_name = "user/user-about.html" def get_awards(self, ratings): result = {} - sorted_ratings = sorted(ratings, - key=lambda x: (x.rank, -x.contest.end_time.timestamp())) + 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] + 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: @@ -160,60 +209,86 @@ class UserAboutPage(UserPage): 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') \ - .defer('contest__description') - - context['rating_data'] = mark_safe(json.dumps([{ - 'label': rating.contest.name, - 'rating': rating.rating, - 'ranking': rating.rank, - 'link': reverse('contest_ranking', args=(rating.contest.key,)), - 'timestamp': (rating.contest.end_time - EPOCH).total_seconds() * 1000, - 'date': date_format(timezone.localtime(rating.contest.end_time), _('M j, Y, G:i')), - 'class': rating_class(rating.rating), - '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')) - min_ever, max_ever = global_data['rating__min'], global_data['rating__max'] - min_user, max_user = user_data['rating__min'], user_data['rating__max'] - delta = max_user - min_user - ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0 - context['max_graph'] = max_user + ratio * delta - context['min_graph'] = min_user + ratio * delta - delta - - - submissions = ( - self.object.submission_set - .annotate(date_only=Cast('date', DateField())) - .values('date_only').annotate(cnt=Count('id')) + ratings = context["ratings"] = ( + self.object.ratings.order_by("-contest__end_time") + .select_related("contest") + .defer("contest__description") + ) + + context["rating_data"] = mark_safe( + json.dumps( + [ + { + "label": rating.contest.name, + "rating": rating.rating, + "ranking": rating.rank, + "link": reverse("contest_ranking", args=(rating.contest.key,)), + "timestamp": (rating.contest.end_time - EPOCH).total_seconds() + * 1000, + "date": date_format( + timezone.localtime(rating.contest.end_time), + _("M j, Y, G:i"), + ), + "class": rating_class(rating.rating), + "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")) + min_ever, max_ever = global_data["rating__min"], global_data["rating__max"] + min_user, max_user = user_data["rating__min"], user_data["rating__max"] + delta = max_user - min_user + ratio = ( + (max_ever - max_user) / (max_ever - min_ever) + if max_ever != min_ever + else 1.0 + ) + context["max_graph"] = max_user + ratio * delta + context["min_graph"] = min_user + ratio * delta - delta + + submissions = ( + self.object.submission_set.annotate(date_only=Cast("date", DateField())) + .values("date_only") + .annotate(cnt=Count("id")) + ) + + context["submission_data"] = mark_safe( + json.dumps( + { + date_counts["date_only"].isoformat(): date_counts["cnt"] + for date_counts in submissions + } + ) + ) + context["submission_metadata"] = mark_safe( + json.dumps( + { + "min_year": ( + self.object.submission_set.annotate( + year_only=ExtractYear("date") + ).aggregate(min_year=Min("year_only"))["min_year"] + ), + } + ) ) - context['submission_data'] = mark_safe(json.dumps({ - date_counts['date_only'].isoformat(): date_counts['cnt'] for date_counts in submissions - })) - context['submission_metadata'] = mark_safe(json.dumps({ - 'min_year': ( - self.object.submission_set - .annotate(year_only=ExtractYear('date')) - .aggregate(min_year=Min('year_only'))['min_year'] - ), - })) - return context # follow/unfollow user def post(self, request, user, *args, **kwargs): try: if not request.profile: - raise Exception('You have to login') - if (request.profile.username == user): - raise Exception('Cannot make friend with yourself') - + raise Exception("You have to login") + if request.profile.username == user: + raise Exception("Cannot make friend with yourself") + following_profile = Profile.objects.get(user__username=user) Friend.toggle_friend(request.profile, following_profile) finally: @@ -221,60 +296,86 @@ class UserAboutPage(UserPage): class UserProblemsPage(UserPage): - template_name = 'user/user-problems.html' + template_name = "user/user-problems.html" def get_context_data(self, **kwargs): context = super(UserProblemsPage, self).get_context_data(**kwargs) - result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True, - problem__is_organization_private=False) \ - .exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \ - .values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \ - .distinct().annotate(points=Max('points')).order_by('problem__group__full_name', 'problem__code') + result = ( + Submission.objects.filter( + user=self.object, + points__gt=0, + problem__is_public=True, + problem__is_organization_private=False, + ) + .exclude( + problem__in=self.get_completed_problems() if self.hide_solved else [] + ) + .values( + "problem__id", + "problem__code", + "problem__name", + "problem__points", + "problem__group__full_name", + ) + .distinct() + .annotate(points=Max("points")) + .order_by("problem__group__full_name", "problem__code") + ) def process_group(group, problems_iter): problems = list(problems_iter) - points = sum(map(itemgetter('points'), problems)) - return {'name': group, 'problems': problems, 'points': points} + points = sum(map(itemgetter("points"), problems)) + return {"name": group, "problems": problems, "points": points} - context['best_submissions'] = [ - process_group(group, problems) for group, problems in itertools.groupby( - remap_keys(result, { - 'problem__code': 'code', 'problem__name': 'name', 'problem__points': 'total', - 'problem__group__full_name': 'group', - }), itemgetter('group')) + context["best_submissions"] = [ + process_group(group, problems) + for group, problems in itertools.groupby( + remap_keys( + result, + { + "problem__code": "code", + "problem__name": "name", + "problem__points": "total", + "problem__group__full_name": "group", + }, + ), + itemgetter("group"), + ) ] breakdown, has_more = get_pp_breakdown(self.object, start=0, end=10) - context['pp_breakdown'] = breakdown - context['pp_has_more'] = has_more + context["pp_breakdown"] = breakdown + context["pp_has_more"] = has_more return context class UserPerformancePointsAjax(UserProblemsPage): - template_name = 'user/pp-table-body.html' + template_name = "user/pp-table-body.html" def get_context_data(self, **kwargs): context = super(UserPerformancePointsAjax, self).get_context_data(**kwargs) try: - start = int(self.request.GET.get('start', 0)) - end = int(self.request.GET.get('end', settings.DMOJ_PP_ENTRIES)) + start = int(self.request.GET.get("start", 0)) + end = int(self.request.GET.get("end", settings.DMOJ_PP_ENTRIES)) if start < 0 or end < 0 or start > end: raise ValueError except ValueError: start, end = 0, 100 breakdown, self.has_more = get_pp_breakdown(self.object, start=start, end=end) - context['pp_breakdown'] = breakdown + context["pp_breakdown"] = breakdown return context def get(self, request, *args, **kwargs): httpresp = super(UserPerformancePointsAjax, self).get(request, *args, **kwargs) httpresp.render() - return JsonResponse({ - 'results': utf8text(httpresp.content), - 'has_more': self.has_more, - }) + return JsonResponse( + { + "results": utf8text(httpresp.content), + "has_more": self.has_more, + } + ) @login_required @@ -282,26 +383,39 @@ def edit_profile(request): profile = Profile.objects.get(user=request.user) if profile.mute: raise Http404() - if request.method == 'POST': + if request.method == "POST": form = ProfileForm(request.POST, instance=profile, user=request.user) if form.is_valid(): with transaction.atomic(), revisions.create_revision(): form.save() revisions.set_user(request.user) - revisions.set_comment(_('Updated on site')) + revisions.set_comment(_("Updated on site")) if newsletter_id is not None: try: - subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id) + subscription = Subscription.objects.get( + user=request.user, newsletter_id=newsletter_id + ) except Subscription.DoesNotExist: - if form.cleaned_data['newsletter']: - Subscription(user=request.user, newsletter_id=newsletter_id, subscribed=True).save() + if form.cleaned_data["newsletter"]: + Subscription( + user=request.user, + newsletter_id=newsletter_id, + subscribed=True, + ).save() else: - if subscription.subscribed != form.cleaned_data['newsletter']: - subscription.update(('unsubscribe', 'subscribe')[form.cleaned_data['newsletter']]) + if subscription.subscribed != form.cleaned_data["newsletter"]: + subscription.update( + ("unsubscribe", "subscribe")[ + form.cleaned_data["newsletter"] + ] + ) - perm = Permission.objects.get(codename='test_site', content_type=ContentType.objects.get_for_model(Profile)) - if form.cleaned_data['test_site']: + perm = Permission.objects.get( + codename="test_site", + content_type=ContentType.objects.get_for_model(Profile), + ) + if form.cleaned_data["test_site"]: request.user.user_permissions.add(perm) else: request.user.user_permissions.remove(perm) @@ -311,34 +425,42 @@ def edit_profile(request): form = ProfileForm(instance=profile, user=request.user) if newsletter_id is not None: try: - subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id) + subscription = Subscription.objects.get( + user=request.user, newsletter_id=newsletter_id + ) except Subscription.DoesNotExist: - form.fields['newsletter'].initial = False + form.fields["newsletter"].initial = False else: - form.fields['newsletter'].initial = subscription.subscribed - form.fields['test_site'].initial = request.user.has_perm('judge.test_site') + form.fields["newsletter"].initial = subscription.subscribed + 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), - 'TIMEZONE_MAP': tzmap or 'http://momentjs.com/static/img/world.png', - 'TIMEZONE_BG': settings.TIMEZONE_BG if tzmap else '#4E7CAD', - }) + 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), + "TIMEZONE_MAP": tzmap or "http://momentjs.com/static/img/world.png", + "TIMEZONE_BG": settings.TIMEZONE_BG if tzmap else "#4E7CAD", + }, + ) class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView): model = Profile - title = gettext_lazy('Leaderboard') - context_object_name = 'users' - template_name = 'user/list.html' + title = gettext_lazy("Leaderboard") + context_object_name = "users" + template_name = "user/list.html" paginate_by = 100 - all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points')) + all_sorts = frozenset(("points", "problem_count", "rating", "performance_points")) default_desc = all_sorts - default_sort = '-performance_points' + default_sort = "-performance_points" def filter_friend_queryset(self, queryset): friends = list(self.request.profile.get_friends()) @@ -346,18 +468,30 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView): return ret def get_queryset(self): - ret = Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') \ - .only('display_rank', 'user__username', 'points', 'rating', 'performance_points', - 'problem_count') + ret = ( + Profile.objects.filter(is_unlisted=False) + .order_by(self.order, "id") + .select_related("user") + .only( + "display_rank", + "user__username", + "points", + "rating", + "performance_points", + "problem_count", + ) + ) - if (self.request.GET.get('friend') == 'true') and self.request.profile: + if (self.request.GET.get("friend") == "true") and self.request.profile: ret = self.filter_friend_queryset(ret) return ret def get_context_data(self, **kwargs): context = super(UserList, self).get_context_data(**kwargs) - context['users'] = ranker(context['users'], rank=self.paginate_by * (context['page_obj'].number - 1)) - context['first_page_href'] = '.' + context["users"] = ranker( + context["users"], rank=self.paginate_by * (context["page_obj"].number - 1) + ) + context["first_page_href"] = "." context.update(self.get_sort_context()) context.update(self.get_sort_paginate_context()) return context @@ -378,27 +512,36 @@ def users(request): if request.in_contest_mode: participation = request.profile.current_contest contest = participation.contest - return FixedContestRanking.as_view(contest=contest)(request, contest=contest.key) + return FixedContestRanking.as_view(contest=contest)( + request, contest=contest.key + ) return user_list_view(request) def user_ranking_redirect(request): try: - username = request.GET['handle'] + username = request.GET["handle"] except KeyError: raise Http404() user = get_object_or_404(Profile, user__username=username) - rank = Profile.objects.filter(is_unlisted=False, performance_points__gt=user.performance_points).count() + rank = Profile.objects.filter( + is_unlisted=False, performance_points__gt=user.performance_points + ).count() rank += Profile.objects.filter( - is_unlisted=False, performance_points__exact=user.performance_points, id__lt=user.id, + is_unlisted=False, + performance_points__exact=user.performance_points, + id__lt=user.id, ).count() page = rank // UserList.paginate_by - return HttpResponseRedirect('%s%s#!%s' % (reverse('user_list'), '?page=%d' % (page + 1) if page else '', username)) + return HttpResponseRedirect( + "%s%s#!%s" + % (reverse("user_list"), "?page=%d" % (page + 1) if page else "", username) + ) class UserLogoutView(TitleMixin, TemplateView): - template_name = 'registration/logout.html' - title = 'You have been successfully logged out.' + template_name = "registration/logout.html" + title = "You have been successfully logged out." def post(self, request, *args, **kwargs): auth_logout(request) @@ -406,8 +549,8 @@ class UserLogoutView(TitleMixin, TemplateView): class ImportUsersView(TitleMixin, TemplateView): - template_name = 'user/import/index.html' - title = _('Import Users') + template_name = "user/import/index.html" + title = _("Import Users") def get(self, *args, **kwargs): if self.request.user.is_superuser: @@ -416,43 +559,38 @@ class ImportUsersView(TitleMixin, TemplateView): def import_users_post_file(request): - if not request.user.is_superuser or request.method != 'POST': + if not request.user.is_superuser or request.method != "POST": return HttpResponseForbidden() - users = import_users.csv_to_dict(request.FILES['csv_file']) + 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 - }) - + 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': + + if not request.user.is_superuser or request.method != "POST": return HttpResponseForbidden() - users = json.loads(request.body)['users'] + users = json.loads(request.body)["users"] log = import_users.import_users(users) - return JsonResponse({ - 'msg': log - }) + return JsonResponse({"msg": log}) def sample_import_users(request): - if not request.user.is_superuser or request.method != 'GET': + 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 \ No newline at end of file + 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 diff --git a/judge/views/volunteer.py b/judge/views/volunteer.py index 28c9d80..64c58f9 100644 --- a/judge/views/volunteer.py +++ b/judge/views/volunteer.py @@ -5,17 +5,17 @@ from judge.models import VolunteerProblemVote, Problem, ProblemType def vote_problem(request): - if not request.user or not request.user.has_perm('judge.suggest_problem_changes'): + if not request.user or not request.user.has_perm("judge.suggest_problem_changes"): return HttpResponseBadRequest() - if not request.method == 'POST': + if not request.method == "POST": return HttpResponseBadRequest() try: - types_id = request.POST.getlist('types[]') + types_id = request.POST.getlist("types[]") types = ProblemType.objects.filter(id__in=types_id) - problem = Problem.objects.get(code=request.POST['problem']) - knowledge_points = request.POST['knowledge_points'] - thinking_points = request.POST['thinking_points'] - feedback = request.POST['feedback'] + problem = Problem.objects.get(code=request.POST["problem"]) + knowledge_points = request.POST["knowledge_points"] + thinking_points = request.POST["thinking_points"] + feedback = request.POST["feedback"] except Exception as e: return HttpResponseBadRequest() @@ -23,7 +23,7 @@ def vote_problem(request): vote, _ = VolunteerProblemVote.objects.get_or_create( voter=request.profile, problem=problem, - defaults={'knowledge_points': 0, 'thinking_points': 0}, + defaults={"knowledge_points": 0, "thinking_points": 0}, ) vote.knowledge_points = knowledge_points vote.thinking_points = thinking_points diff --git a/judge/views/widgets.py b/judge/views/widgets.py index 57fe15b..f746704 100644 --- a/judge/views/widgets.py +++ b/judge/views/widgets.py @@ -2,60 +2,90 @@ import requests from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import ImproperlyConfigured -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseRedirect, +) from django.utils.translation import gettext as _ from django.views.generic import View from judge.models import Submission -__all__ = ['rejudge_submission', 'DetectTimezone'] +__all__ = ["rejudge_submission", "DetectTimezone"] @login_required def rejudge_submission(request): - if request.method != 'POST' or not request.user.has_perm('judge.rejudge_submission') or \ - not request.user.has_perm('judge.edit_own_problem'): + if ( + request.method != "POST" + or not request.user.has_perm("judge.rejudge_submission") + or not request.user.has_perm("judge.edit_own_problem") + ): return HttpResponseForbidden() - if 'id' not in request.POST or not request.POST['id'].isdigit(): + if "id" not in request.POST or not request.POST["id"].isdigit(): return HttpResponseBadRequest() try: - submission = Submission.objects.get(id=request.POST['id']) + submission = Submission.objects.get(id=request.POST["id"]) except Submission.DoesNotExist: return HttpResponseBadRequest() - if not request.user.has_perm('judge.edit_all_problem') and \ - not submission.problem.is_editor(request.profile): + if not request.user.has_perm( + "judge.edit_all_problem" + ) and not submission.problem.is_editor(request.profile): return HttpResponseForbidden() submission.judge(rejudge=True) - redirect = request.POST.get('path', None) + redirect = request.POST.get("path", None) - return HttpResponseRedirect(redirect) if redirect else HttpResponse('success', content_type='text/plain') + return ( + HttpResponseRedirect(redirect) + if redirect + else HttpResponse("success", content_type="text/plain") + ) class DetectTimezone(View): def askgeo(self, lat, long): - if not hasattr(settings, 'ASKGEO_ACCOUNT_ID') or not hasattr(settings, 'ASKGEO_ACCOUNT_API_KEY'): + if not hasattr(settings, "ASKGEO_ACCOUNT_ID") or not hasattr( + settings, "ASKGEO_ACCOUNT_API_KEY" + ): raise ImproperlyConfigured() - data = requests.get('http://api.askgeo.com/v1/%s/%s/query.json?databases=TimeZone&points=%f,%f' % - (settings.ASKGEO_ACCOUNT_ID, settings.ASKGEO_ACCOUNT_API_KEY, lat, long)).json() + data = requests.get( + "http://api.askgeo.com/v1/%s/%s/query.json?databases=TimeZone&points=%f,%f" + % (settings.ASKGEO_ACCOUNT_ID, settings.ASKGEO_ACCOUNT_API_KEY, lat, long) + ).json() try: - return HttpResponse(data['data'][0]['TimeZone']['TimeZoneId'], content_type='text/plain') + return HttpResponse( + data["data"][0]["TimeZone"]["TimeZoneId"], content_type="text/plain" + ) except (IndexError, KeyError): - return HttpResponse(_('Invalid upstream data: %s') % data, content_type='text/plain', status=500) + return HttpResponse( + _("Invalid upstream data: %s") % data, + content_type="text/plain", + status=500, + ) def geonames(self, lat, long): - if not hasattr(settings, 'GEONAMES_USERNAME'): + if not hasattr(settings, "GEONAMES_USERNAME"): raise ImproperlyConfigured() - data = requests.get('http://api.geonames.org/timezoneJSON?lat=%f&lng=%f&username=%s' % - (lat, long, settings.GEONAMES_USERNAME)).json() + data = requests.get( + "http://api.geonames.org/timezoneJSON?lat=%f&lng=%f&username=%s" + % (lat, long, settings.GEONAMES_USERNAME) + ).json() try: - return HttpResponse(data['timezoneId'], content_type='text/plain') + return HttpResponse(data["timezoneId"], content_type="text/plain") except KeyError: - return HttpResponse(_('Invalid upstream data: %s') % data, content_type='text/plain', status=500) + return HttpResponse( + _("Invalid upstream data: %s") % data, + content_type="text/plain", + status=500, + ) def default(self, lat, long): raise Http404() @@ -63,10 +93,11 @@ class DetectTimezone(View): def get(self, request, *args, **kwargs): backend = settings.TIMEZONE_DETECT_BACKEND try: - lat, long = float(request.GET['lat']), float(request.GET['long']) + lat, long = float(request.GET["lat"]), float(request.GET["long"]) except (ValueError, KeyError): - return HttpResponse(_('Bad latitude or longitude'), content_type='text/plain', status=404) - return { - 'askgeo': self.askgeo, - 'geonames': self.geonames, - }.get(backend, self.default)(lat, long) + return HttpResponse( + _("Bad latitude or longitude"), content_type="text/plain", status=404 + ) + return {"askgeo": self.askgeo, "geonames": self.geonames,}.get( + backend, self.default + )(lat, long) diff --git a/judge/widgets/checkbox.py b/judge/widgets/checkbox.py index 0a7eebf..b4b2249 100644 --- a/judge/widgets/checkbox.py +++ b/judge/widgets/checkbox.py @@ -6,17 +6,25 @@ from django.utils.safestring import mark_safe class CheckboxSelectMultipleWithSelectAll(forms.CheckboxSelectMultiple): def render(self, name, value, attrs=None, renderer=None): - if 'id' not in attrs: - raise FieldError('id required') + if "id" not in attrs: + raise FieldError("id required") - select_all_id = attrs['id'] + '_all' - select_all_name = name + '_all' - original = super(CheckboxSelectMultipleWithSelectAll, self).render(name, value, attrs, renderer) - template = get_template('widgets/select_all.html') - return mark_safe(template.render({ - 'original_widget': original, - 'select_all_id': select_all_id, - 'select_all_name': select_all_name, - 'all_selected': all(choice[0] in value for choice in self.choices) if value else False, - 'empty': not self.choices, - })) + select_all_id = attrs["id"] + "_all" + select_all_name = name + "_all" + original = super(CheckboxSelectMultipleWithSelectAll, self).render( + name, value, attrs, renderer + ) + template = get_template("widgets/select_all.html") + return mark_safe( + template.render( + { + "original_widget": original, + "select_all_id": select_all_id, + "select_all_name": select_all_name, + "all_selected": all(choice[0] in value for choice in self.choices) + if value + else False, + "empty": not self.choices, + } + ) + ) diff --git a/judge/widgets/mixins.py b/judge/widgets/mixins.py index 0270276..6317bac 100644 --- a/judge/widgets/mixins.py +++ b/judge/widgets/mixins.py @@ -7,23 +7,27 @@ from lxml import html class CompressorWidgetMixin(object): - __template_css = dedent('''\ + __template_css = dedent( + """\ {% compress css %} {{ media.css }} {% endcompress %} - ''') + """ + ) - __template_js = dedent('''\ + __template_js = dedent( + """\ {% compress js %} {{ media.js }} {% endcompress %} - ''') + """ + ) __templates = { - (False, False): Template(''), - (True, False): Template('{% load compress %}' + __template_css), - (False, True): Template('{% load compress %}' + __template_js), - (True, True): Template('{% load compress %}' + __template_js + __template_css), + (False, False): Template(""), + (True, False): Template("{% load compress %}" + __template_css), + (False, True): Template("{% load compress %}" + __template_js), + (True, True): Template("{% load compress %}" + __template_js + __template_css), } compress_css = False @@ -34,14 +38,19 @@ class CompressorWidgetMixin(object): except ImportError: pass else: - if getattr(settings, 'COMPRESS_ENABLED', not settings.DEBUG): + if getattr(settings, "COMPRESS_ENABLED", not settings.DEBUG): + @property def media(self): media = super().media template = self.__templates[self.compress_css, self.compress_js] - result = html.fromstring(template.render(Context({'media': media}))) + result = html.fromstring(template.render(Context({"media": media}))) return forms.Media( - css={'all': [result.find('.//link').get('href')]} if self.compress_css else media._css, - js=[result.find('.//script').get('src')] if self.compress_js else media._js, + css={"all": [result.find(".//link").get("href")]} + if self.compress_css + else media._css, + js=[result.find(".//script").get("src")] + if self.compress_js + else media._js, ) diff --git a/judge/widgets/pagedown.py b/judge/widgets/pagedown.py index 0968107..3225dc7 100644 --- a/judge/widgets/pagedown.py +++ b/judge/widgets/pagedown.py @@ -6,9 +6,14 @@ from django.utils.html import conditional_escape from judge.widgets.mixins import CompressorWidgetMixin -__all__ = ['PagedownWidget', 'AdminPagedownWidget', - 'MathJaxPagedownWidget', 'MathJaxAdminPagedownWidget', - 'HeavyPreviewPageDownWidget', 'HeavyPreviewAdminPageDownWidget'] +__all__ = [ + "PagedownWidget", + "AdminPagedownWidget", + "MathJaxPagedownWidget", + "MathJaxAdminPagedownWidget", + "HeavyPreviewPageDownWidget", + "HeavyPreviewAdminPageDownWidget", +] try: from pagedown.widgets import PagedownWidget as OldPagedownWidget @@ -20,6 +25,7 @@ except ImportError: HeavyPreviewPageDownWidget = None HeavyPreviewAdminPageDownWidget = None else: + class PagedownWidget(CompressorWidgetMixin, OldPagedownWidget): # The goal here is to compress all the pagedown JS into one file. # We do not want any further compress down the chain, because @@ -28,23 +34,25 @@ else: compress_js = True def __init__(self, *args, **kwargs): - kwargs.setdefault('css', ('pagedown_widget.css',)) + kwargs.setdefault("css", ("pagedown_widget.css",)) super(PagedownWidget, self).__init__(*args, **kwargs) class AdminPagedownWidget(PagedownWidget, admin_widgets.AdminTextareaWidget): class Media: - css = {'all': [ - 'content-description.css', - 'admin/css/pagedown.css', - ]} - js = ['admin/js/pagedown.js'] + css = { + "all": [ + "content-description.css", + "admin/css/pagedown.css", + ] + } + js = ["admin/js/pagedown.js"] class MathJaxPagedownWidget(PagedownWidget): class Media: js = [ - 'mathjax_config.js', - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML', - 'pagedown_math.js', + "mathjax_config.js", + "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML", + "pagedown_math.js", ] class MathJaxAdminPagedownWidget(AdminPagedownWidget, MathJaxPagedownWidget): @@ -52,40 +60,46 @@ else: class HeavyPreviewPageDownWidget(PagedownWidget): def __init__(self, *args, **kwargs): - kwargs.setdefault('template', 'pagedown.html') - self.preview_url = kwargs.pop('preview') - self.preview_timeout = kwargs.pop('preview_timeout', None) - self.hide_preview_button = kwargs.pop('hide_preview_button', False) + kwargs.setdefault("template", "pagedown.html") + self.preview_url = kwargs.pop("preview") + self.preview_timeout = kwargs.pop("preview_timeout", None) + self.hide_preview_button = kwargs.pop("hide_preview_button", False) super(HeavyPreviewPageDownWidget, self).__init__(*args, **kwargs) def render(self, name, value, attrs=None, renderer=None): if value is None: - value = '' - final_attrs = self.build_attrs(attrs, {'name': name}) - if 'class' not in final_attrs: - final_attrs['class'] = '' - final_attrs['class'] += ' wmd-input' - return get_template(self.template).render(self.get_template_context(final_attrs, value)) + value = "" + final_attrs = self.build_attrs(attrs, {"name": name}) + if "class" not in final_attrs: + final_attrs["class"] = "" + final_attrs["class"] += " wmd-input" + return get_template(self.template).render( + self.get_template_context(final_attrs, value) + ) def get_template_context(self, attrs, value): return { - 'attrs': flatatt(attrs), - 'body': conditional_escape(force_text(value)), - 'id': attrs['id'], - 'show_preview': self.show_preview, - 'preview_url': self.preview_url, - 'preview_timeout': self.preview_timeout, - 'extra_classes': 'dmmd-no-button' if self.hide_preview_button else None, + "attrs": flatatt(attrs), + "body": conditional_escape(force_text(value)), + "id": attrs["id"], + "show_preview": self.show_preview, + "preview_url": self.preview_url, + "preview_timeout": self.preview_timeout, + "extra_classes": "dmmd-no-button" if self.hide_preview_button else None, } class Media: - css = {'all': ['dmmd-preview.css']} - js = ['dmmd-preview.js'] + css = {"all": ["dmmd-preview.css"]} + js = ["dmmd-preview.js"] - class HeavyPreviewAdminPageDownWidget(AdminPagedownWidget, HeavyPreviewPageDownWidget): + class HeavyPreviewAdminPageDownWidget( + AdminPagedownWidget, HeavyPreviewPageDownWidget + ): class Media: - css = {'all': [ - 'pygment-github.css', - 'table.css', - 'ranks.css', - ]} + css = { + "all": [ + "pygment-github.css", + "table.css", + "ranks.css", + ] + } diff --git a/judge/widgets/select2.py b/judge/widgets/select2.py index af4843b..6a7db7f 100644 --- a/judge/widgets/select2.py +++ b/judge/widgets/select2.py @@ -46,13 +46,23 @@ from django.core import signing from django.forms.models import ModelChoiceIterator from django.urls import reverse_lazy -DEFAULT_SELECT2_JS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' -DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' +DEFAULT_SELECT2_JS = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js" +DEFAULT_SELECT2_CSS = ( + "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" +) -__all__ = ['Select2Widget', 'Select2MultipleWidget', 'Select2TagWidget', - 'HeavySelect2Widget', 'HeavySelect2MultipleWidget', 'HeavySelect2TagWidget', - 'AdminSelect2Widget', 'AdminSelect2MultipleWidget', 'AdminHeavySelect2Widget', - 'AdminHeavySelect2MultipleWidget'] +__all__ = [ + "Select2Widget", + "Select2MultipleWidget", + "Select2TagWidget", + "HeavySelect2Widget", + "HeavySelect2MultipleWidget", + "HeavySelect2TagWidget", + "AdminSelect2Widget", + "AdminSelect2MultipleWidget", + "AdminHeavySelect2Widget", + "AdminHeavySelect2MultipleWidget", +] class Select2Mixin(object): @@ -68,22 +78,22 @@ class Select2Mixin(object): """Add select2 data attributes.""" attrs = super(Select2Mixin, self).build_attrs(base_attrs, extra_attrs) if self.is_required: - attrs.setdefault('data-allow-clear', 'false') + attrs.setdefault("data-allow-clear", "false") else: - attrs.setdefault('data-allow-clear', 'true') - attrs.setdefault('data-placeholder', '') + attrs.setdefault("data-allow-clear", "true") + attrs.setdefault("data-placeholder", "") - attrs.setdefault('data-minimum-input-length', 0) - if 'class' in attrs: - attrs['class'] += ' django-select2' + attrs.setdefault("data-minimum-input-length", 0) + if "class" in attrs: + attrs["class"] += " django-select2" else: - attrs['class'] = 'django-select2' + attrs["class"] = "django-select2" return attrs def optgroups(self, name, value, attrs=None): """Add empty option for clearable selects.""" if not self.is_required and not self.allow_multiple_selected: - self.choices = list(chain([('', '')], self.choices)) + self.choices = list(chain([("", "")], self.choices)) return super(Select2Mixin, self).optgroups(name, value, attrs=attrs) @property @@ -95,8 +105,8 @@ class Select2Mixin(object): https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property """ return forms.Media( - js=[settings.SELECT2_JS_URL, 'django_select2.js'], - css={'screen': [settings.SELECT2_CSS_URL]}, + js=[settings.SELECT2_JS_URL, "django_select2.js"], + css={"screen": [settings.SELECT2_CSS_URL]}, ) @@ -104,8 +114,12 @@ class AdminSelect2Mixin(Select2Mixin): @property def media(self): return forms.Media( - js=['admin/js/jquery.init.js', settings.SELECT2_JS_URL, 'django_select2.js'], - css={'screen': [settings.SELECT2_CSS_URL]}, + js=[ + "admin/js/jquery.init.js", + settings.SELECT2_JS_URL, + "django_select2.js", + ], + css={"screen": [settings.SELECT2_CSS_URL]}, ) @@ -115,9 +129,9 @@ class Select2TagMixin(object): def build_attrs(self, base_attrs, extra_attrs=None): """Add select2's tag attributes.""" extra_attrs = extra_attrs or {} - extra_attrs.setdefault('data-minimum-input-length', 1) - extra_attrs.setdefault('data-tags', 'true') - extra_attrs.setdefault('data-token-separators', [",", " "]) + extra_attrs.setdefault("data-minimum-input-length", 1) + extra_attrs.setdefault("data-tags", "true") + extra_attrs.setdefault("data-token-separators", [",", " "]) return super(Select2TagMixin, self).build_attrs(base_attrs, extra_attrs) @@ -182,8 +196,8 @@ class HeavySelect2Mixin(Select2Mixin): else: self.attrs = {} - self.data_view = kwargs.pop('data_view', None) - self.data_url = kwargs.pop('data_url', None) + self.data_view = kwargs.pop("data_view", None) + self.data_url = kwargs.pop("data_url", None) if not (self.data_view or self.data_url): raise ValueError('You must ether specify "data_view" or "data_url".') @@ -201,22 +215,22 @@ class HeavySelect2Mixin(Select2Mixin): # encrypt instance Id self.widget_id = signing.dumps(id(self)) - attrs['data-field_id'] = self.widget_id - attrs.setdefault('data-ajax--url', self.get_url()) - attrs.setdefault('data-ajax--cache', "true") - attrs.setdefault('data-ajax--type', "GET") - attrs.setdefault('data-minimum-input-length', 2) + attrs["data-field_id"] = self.widget_id + attrs.setdefault("data-ajax--url", self.get_url()) + attrs.setdefault("data-ajax--cache", "true") + attrs.setdefault("data-ajax--type", "GET") + attrs.setdefault("data-minimum-input-length", 2) - attrs['class'] += ' django-select2-heavy' + attrs["class"] += " django-select2-heavy" return attrs def format_value(self, value): result = super(HeavySelect2Mixin, self).format_value(value) if isinstance(self.choices, ModelChoiceIterator): chosen = copy(self.choices) - chosen.queryset = chosen.queryset.filter(pk__in=[ - int(i) for i in result if isinstance(i, int) or i.isdigit() - ]) + chosen.queryset = chosen.queryset.filter( + pk__in=[int(i) for i in result if isinstance(i, int) or i.isdigit()] + ) self.choices = set(chosen) return result diff --git a/manage.py b/manage.py index 498e387..f0470c1 100644 --- a/manage.py +++ b/manage.py @@ -11,6 +11,7 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") from django.core.management import execute_from_command_line + # noinspection PyUnresolvedReferences import django_2_2_pymysql_patch # noqa: F401, imported for side effect diff --git a/validator_template/template.py b/validator_template/template.py index 9144f70..af55fb5 100644 --- a/validator_template/template.py +++ b/validator_template/template.py @@ -4,7 +4,11 @@ import subprocess from dmoj.error import InternalError from dmoj.judgeenv import env, get_problem_root from dmoj.result import CheckerResult -from dmoj.utils.helper_files import compile_with_auxiliary_files, mktemp, parse_helper_file_error +from dmoj.utils.helper_files import ( + compile_with_auxiliary_files, + mktemp, + parse_helper_file_error, +) from dmoj.utils.unicode import utf8text executor = None @@ -17,60 +21,93 @@ def get_executor(files, lang, compiler_time_limit, problem_id): if not isinstance(files, list): files = [files] filenames = [os.path.join(get_problem_root(problem_id), f) for f in files] - executor = compile_with_auxiliary_files(filenames, - compiler_time_limit=compiler_time_limit) + executor = compile_with_auxiliary_files( + filenames, compiler_time_limit=compiler_time_limit + ) return executor + class Module: AC = 0 WA = 1 PARTIAL = 2 # a float from 0 to 1 - repartial = re.compile(r'^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$', re.M) + repartial = re.compile(r"^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$", re.M) @classmethod - def parse_return_code(cls, proc, executor, point_value, time_limit, memory_limit, feedback, name, stderr): + def parse_return_code( + cls, + proc, + executor, + point_value, + time_limit, + memory_limit, + feedback, + name, + stderr, + ): if proc.returncode == cls.AC: return CheckerResult(True, point_value, feedback=feedback) elif proc.returncode == cls.PARTIAL: match = cls.repartial.search(stderr) if not match: - raise InternalError('Invalid stderr for partial points: %r' % stderr) + raise InternalError("Invalid stderr for partial points: %r" % stderr) points = float(match.group(0)) if not 0 <= points <= 1: - raise InternalError('Invalid partial points: %d' % points) - - ac = (points == 1) + raise InternalError("Invalid partial points: %d" % points) + + ac = points == 1 return CheckerResult(ac, points * point_value, feedback=feedback) elif proc.returncode == cls.WA: return CheckerResult(False, 0, feedback=feedback) else: - parse_helper_file_error(proc, executor, name, stderr, time_limit, memory_limit) + parse_helper_file_error( + proc, executor, name, stderr, time_limit, memory_limit + ) -def check(process_output, judge_output, judge_input, - problem_id={{problemid}}, - files={{filecpp}}, - lang='CPP14', - time_limit=10, - memory_limit=1024 * 512, - compiler_time_limit=30, - feedback=True, - point_value=None, **kwargs) -> CheckerResult: +def check( + process_output, + judge_output, + judge_input, + problem_id={{problemid}}, + files={{filecpp}}, + lang="CPP14", + time_limit=10, + memory_limit=1024 * 512, + compiler_time_limit=30, + feedback=True, + point_value=None, + **kwargs +) -> CheckerResult: executor = get_executor(files, lang, compiler_time_limit, problem_id) - with mktemp(judge_input) as input_file, mktemp(process_output) as output_file, mktemp(judge_output) as judge_file: - try: - process = executor.launch(input_file.name, output_file.name, judge_file.name, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, memory=memory_limit, time=time_limit) + with mktemp(judge_input) as input_file, mktemp( + process_output + ) as output_file, mktemp(judge_output) as judge_file: + try: + process = executor.launch( + input_file.name, + output_file.name, + judge_file.name, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + memory=memory_limit, + time=time_limit, + ) proc_output, error = map(utf8text, process.communicate()) except Exception as err: - raise InternalError('Error while running checker: %r', err) + raise InternalError("Error while running checker: %r", err) - return Module.parse_return_code(process, executor, point_value, time_limit, - memory_limit, - feedback=utf8text(proc_output) - if feedback else None, name='checker', - stderr=error) \ No newline at end of file + return Module.parse_return_code( + process, + executor, + point_value, + time_limit, + memory_limit, + feedback=utf8text(proc_output) if feedback else None, + name="checker", + stderr=error, + )