Merge branch 'LQDJudge:master' into master

This commit is contained in:
Bao Le 2023-11-21 14:21:00 +08:00 committed by GitHub
commit 0403c4be31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 2242 additions and 1361 deletions

View file

@ -11,3 +11,7 @@ repos:
rev: 22.12.0 rev: 22.12.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/hadialqattan/pycln
rev: 'v2.3.0'
hooks:
- id: pycln

View file

@ -0,0 +1,33 @@
# Generated by Django 3.2.18 on 2023-11-02 01:41
from django.db import migrations, models
def migrate(apps, schema_editor):
Room = apps.get_model("chat_box", "Room")
Message = apps.get_model("chat_box", "Message")
for room in Room.objects.all():
messages = room.message_set
last_msg = messages.first()
if last_msg:
room.last_msg_time = last_msg.time
room.save()
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0014_userroom_unread_count"),
]
operations = [
migrations.AddField(
model_name="room",
name="last_msg_time",
field=models.DateTimeField(
db_index=True, null=True, verbose_name="last seen"
),
),
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
]

View file

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.db.models import CASCADE, Q from django.db.models import CASCADE, Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile from judge.models.profile import Profile
@ -17,25 +18,40 @@ class Room(models.Model):
user_two = models.ForeignKey( user_two = models.ForeignKey(
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
) )
last_msg_time = models.DateTimeField(
verbose_name=_("last seen"), null=True, db_index=True
)
class Meta: class Meta:
app_label = "chat_box" app_label = "chat_box"
@cache_wrapper(prefix="Rc") @cache_wrapper(prefix="Rinfo")
def contain(self, profile): def _info(self):
return self.user_one == profile or self.user_two == profile last_msg = self.message_set.first()
return {
"user_ids": [self.user_one.id, self.user_two.id],
"last_message": last_msg.body if last_msg else None,
}
@cached_property
def _cached_info(self):
return self._info()
def contain(self, profile):
return profile.id in self._cached_info["user_ids"]
@cache_wrapper(prefix="Rou")
def other_user(self, profile): def other_user(self, profile):
return self.user_one if profile == self.user_two else self.user_two return self.user_one if profile == self.user_two else self.user_two
@cache_wrapper(prefix="Rus") def other_user_id(self, profile):
user_ids = self._cached_info["user_ids"]
return sum(user_ids) - profile.id
def users(self): def users(self):
return [self.user_one, self.user_two] return [self.user_one, self.user_two]
@cache_wrapper(prefix="Rlmb")
def last_message_body(self): def last_message_body(self):
return self.message_set.first().body return self._cached_info["last_message"]
class Message(models.Model): class Message(models.Model):

View file

@ -29,7 +29,6 @@ from django.utils import timezone
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import reverse from django.urls import reverse
import datetime
from judge import event_poster as event from judge import event_poster as event
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
@ -38,8 +37,6 @@ from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore from chat_box.models import Message, Profile, Room, UserRoom, Ignore
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
import json
class ChatView(ListView): class ChatView(ListView):
context_object_name = "message" context_object_name = "message"
@ -87,8 +84,8 @@ class ChatView(ListView):
self.room_id = request_room self.room_id = request_room
self.messages = ( self.messages = (
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id) Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
.select_related("author", "author__user") .select_related("author")
.defer("author__about", "author__user_script")[:page_size] .only("body", "time", "author__rating", "author__display_rank")[:page_size]
) )
if not only_messages: if not only_messages:
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -207,7 +204,9 @@ def post_message(request):
}, },
) )
else: else:
Room.last_message_body.dirty(room) Room._info.dirty(room)
room.last_msg_time = new_message.time
room.save()
for user in room.users(): for user in room.users():
event.post( event.post(
@ -354,11 +353,11 @@ def get_online_status(profile, other_profile_ids, rooms=None):
room = Room.objects.get(id=i["room"]) room = Room.objects.get(id=i["room"])
other_profile = room.other_user(profile) other_profile = room.other_user(profile)
count[other_profile.id] = i["unread_count"] count[other_profile.id] = i["unread_count"]
rooms = Room.objects.filter(id__in=rooms)
for room in rooms: for room in rooms:
room = Room.objects.get(id=room) other_profile_id = room.other_user_id(profile)
other_profile = room.other_user(profile) last_msg[other_profile_id] = room.last_message_body()
last_msg[other_profile.id] = room.last_message_body() room_of_user[other_profile_id] = room.id
room_of_user[other_profile.id] = room.id
for other_profile in other_profiles: for other_profile in other_profiles:
is_online = False is_online = False
@ -392,9 +391,6 @@ def get_status_context(profile, include_ignored=False):
recent_profile = ( recent_profile = (
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile)) Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
.annotate( .annotate(
last_msg_time=Subquery(
Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
),
other_user=Case( other_user=Case(
When(user_one=profile, then="user_two"), When(user_one=profile, then="user_two"),
default="user_one", default="user_one",
@ -415,28 +411,15 @@ def get_status_context(profile, include_ignored=False):
.values_list("id", flat=True) .values_list("id", flat=True)
) )
all_user_status = (
queryset.filter(last_access__gte=last_5_minutes)
.annotate(is_online=Case(default=True, output_field=BooleanField()))
.order_by("-rating")
.exclude(id__in=admin_list)
.exclude(id__in=recent_profile_ids)
.values_list("id", flat=True)[:30]
)
return [ return [
{ {
"title": "Recent", "title": _("Recent"),
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms), "user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
}, },
{ {
"title": "Admin", "title": _("Admin"),
"user_list": get_online_status(profile, admin_list), "user_list": get_online_status(profile, admin_list),
}, },
{
"title": "Other",
"user_list": get_online_status(profile, all_user_status),
},
] ]

View file

@ -9,7 +9,6 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
""" """
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import datetime
import os import os
import tempfile import tempfile

View file

@ -1100,6 +1100,11 @@ urlpatterns = [
internal.InternalProblem.as_view(), internal.InternalProblem.as_view(),
name="internal_problem", name="internal_problem",
), ),
url(
r"^problem_votes$",
internal.get_problem_votes,
name="internal_problem_votes",
),
url( url(
r"^request_time$", r"^request_time$",
internal.InternalRequestTime.as_view(), internal.InternalRequestTime.as_view(),

View file

@ -3,7 +3,6 @@ import json
import logging import logging
import threading import threading
import time import time
import os
from collections import deque, namedtuple from collections import deque, namedtuple
from operator import itemgetter from operator import itemgetter
@ -25,6 +24,7 @@ from judge.models import (
Submission, Submission,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.bridge.utils import VanishedSubmission
logger = logging.getLogger("judge.bridge") logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge") json_log = logging.getLogger("judge.json.bridge")
@ -94,12 +94,6 @@ class JudgeHandler(ZlibPacketHandler):
def on_disconnect(self): def on_disconnect(self):
self._stop_ping.set() self._stop_ping.set()
if self._working:
logger.error(
"Judge %s disconnected while handling submission %s",
self.name,
self._working,
)
self.judges.remove(self) self.judges.remove(self)
if self.name is not None: if self.name is not None:
self._disconnected() self._disconnected()
@ -119,16 +113,6 @@ class JudgeHandler(ZlibPacketHandler):
None, None,
0, 0,
) )
# 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): def _authenticate(self, id, key):
try: try:
@ -327,6 +311,9 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source): def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id) data = self.get_related_submission_data(id)
if not data:
self._update_internal_error_submission(id, "Submission vanished")
raise VanishedSubmission()
self._working = id self._working = id
self._working_data = { self._working_data = {
"problem": problem, "problem": problem,
@ -675,8 +662,11 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet) self._free_self(packet)
id = packet["submission-id"] id = packet["submission-id"]
self._update_internal_error_submission(id, packet["message"])
def _update_internal_error_submission(self, id, message):
if Submission.objects.filter(id=id).update( if Submission.objects.filter(id=id).update(
status="IE", result="IE", error=packet["message"] status="IE", result="IE", error=message
): ):
event.post( event.post(
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"} "sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
@ -684,9 +674,9 @@ class JudgeHandler(ZlibPacketHandler):
self._post_update_submission(id, "internal-error", done=True) self._post_update_submission(id, "internal-error", done=True)
json_log.info( json_log.info(
self._make_json_log( self._make_json_log(
packet, sub=id,
action="internal-error", action="internal-error",
message=packet["message"], message=message,
finish=True, finish=True,
result="IE", result="IE",
) )
@ -695,10 +685,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id) logger.warning("Unknown submission: %s", id)
json_log.error( json_log.error(
self._make_json_log( self._make_json_log(
packet, sub=id,
action="internal-error", action="internal-error",
info="unknown submission", info="unknown submission",
message=packet["message"], message=message,
finish=True, finish=True,
result="IE", result="IE",
) )

View file

@ -3,6 +3,8 @@ from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from threading import RLock from threading import RLock
from judge.bridge.utils import VanishedSubmission
try: try:
from llist import dllist from llist import dllist
except ImportError: except ImportError:
@ -39,6 +41,8 @@ class JudgeList(object):
) )
try: try:
judge.submit(id, problem, language, source) judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception: except Exception:
logger.exception( logger.exception(
"Failed to dispatch %d (%s, %s) to %s", "Failed to dispatch %d (%s, %s) to %s",

2
judge/bridge/utils.py Normal file
View file

@ -0,0 +1,2 @@
class VanishedSubmission(Exception):
pass

View file

@ -40,7 +40,10 @@ def cache_wrapper(prefix, timeout=None):
def _get(key): def _get(key):
if not l0_cache: if not l0_cache:
return cache.get(key) return cache.get(key)
return l0_cache.get(key) or cache.get(key) result = l0_cache.get(key)
if result is None:
result = cache.get(key)
return result
def _set_l0(key, value): def _set_l0(key, value):
if l0_cache: if l0_cache:
@ -56,7 +59,7 @@ def cache_wrapper(prefix, timeout=None):
result = _get(cache_key) result = _get(cache_key)
if result is not None: if result is not None:
_set_l0(cache_key, result) _set_l0(cache_key, result)
if result == NONE_RESULT: if type(result) == str and result == NONE_RESULT:
result = None result = None
return result return result
result = func(*args, **kwargs) result = func(*args, **kwargs)

View file

@ -144,14 +144,16 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
target_comment = None target_comment = None
self.object = self.get_object()
if "comment-id" in request.GET: if "comment-id" in request.GET:
comment_id = int(request.GET["comment-id"])
try: try:
comment_id = int(request.GET["comment-id"])
comment_obj = Comment.objects.get(id=comment_id) comment_obj = Comment.objects.get(id=comment_id)
except Comment.DoesNotExist: except (Comment.DoesNotExist, ValueError):
raise Http404
if comment_obj.linked_object != self.object:
raise Http404 raise Http404
target_comment = comment_obj.get_root() target_comment = comment_obj.get_root()
self.object = self.get_object()
return self.render_to_response( return self.render_to_response(
self.get_context_data( self.get_context_data(
object=self.object, object=self.object,

View file

@ -11,7 +11,6 @@ from django.contrib.auth.models import User
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.forms import ( from django.forms import (
CharField, CharField,
@ -52,7 +51,6 @@ from judge.widgets import (
DateTimePickerWidget, DateTimePickerWidget,
ImageWidget, ImageWidget,
) )
from judge.tasks import rescore_contest
def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")): def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
@ -282,16 +280,9 @@ class EditOrganizationContestForm(ModelForm):
"view_contest_scoreboard", "view_contest_scoreboard",
]: ]:
self.fields[field].widget.data_url = ( self.fields[field].widget.data_url = (
self.fields[field].widget.get_url() + "?org_id=1" self.fields[field].widget.get_url() + f"?org_id={self.org_id}"
) )
def save(self, commit=True):
res = super(EditOrganizationContestForm, self).save(commit=False)
if commit:
res.save()
transaction.on_commit(rescore_contest.s(res.key).delay)
return res
class Meta: class Meta:
model = Contest model = Contest
fields = ( fields = (

7
judge/logging.py Normal file
View file

@ -0,0 +1,7 @@
import logging
error_log = logging.getLogger("judge.errors")
def log_exception(msg):
error_log.exception(msg)

View file

@ -1,6 +1,5 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from judge.models import * from judge.models import *
from collections import defaultdict
import csv import csv
import os import os
from django.conf import settings from django.conf import settings

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,10 @@
import numpy as np import numpy as np
from django.conf import settings
import os import os
from django.core.cache import cache
import hashlib import hashlib
from django.core.cache import cache
from django.conf import settings
from judge.caching import cache_wrapper from judge.caching import cache_wrapper
@ -13,14 +14,13 @@ class CollabFilter:
# name = 'collab_filter' or 'collab_filter_time' # name = 'collab_filter' or 'collab_filter_time'
def __init__(self, name): def __init__(self, name):
embeddings = np.load( self.embeddings = np.load(
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"), os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
allow_pickle=True, allow_pickle=True,
) )
arr0, arr1 = embeddings.files _, problem_arr = self.embeddings.files
self.name = name self.name = name
self.user_embeddings = embeddings[arr0] self.problem_embeddings = self.embeddings[problem_arr]
self.problem_embeddings = embeddings[arr1]
def __str__(self): def __str__(self):
return self.name return self.name
@ -44,18 +44,32 @@ class CollabFilter:
scores = u.dot(V.T) scores = u.dot(V.T)
return scores return scores
def _get_embedding_version(self):
first_problem = self.problem_embeddings[0]
array_bytes = first_problem.tobytes()
hash_object = hashlib.sha256(array_bytes)
hash_bytes = hash_object.digest()
return hash_bytes.hex()[:5]
@cache_wrapper(prefix="CFgue", timeout=86400)
def _get_user_embedding(self, user_id, embedding_version):
user_arr, _ = self.embeddings.files
user_embeddings = self.embeddings[user_arr]
if user_id >= len(user_embeddings):
return user_embeddings[0]
return user_embeddings[user_id]
def get_user_embedding(self, user_id):
version = self._get_embedding_version()
return self._get_user_embedding(user_id, version)
@cache_wrapper(prefix="user_recommendations", timeout=3600) @cache_wrapper(prefix="user_recommendations", timeout=3600)
def user_recommendations(self, user, problems, measure=DOT, limit=None): def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
uid = user.id user_embedding = self.get_user_embedding(user_id)
if uid >= len(self.user_embeddings): scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
uid = 0
scores = self.compute_scores(
self.user_embeddings[uid], self.problem_embeddings, measure
)
res = [] # [(score, problem)] res = [] # [(score, problem)]
for pid in problems: for pid in problems:
# pid = problem.id
if pid < len(scores): if pid < len(scores):
res.append((scores[pid], pid)) res.append((scores[pid], pid))

View file

@ -99,11 +99,13 @@ class Contest(models.Model, PageVotable, Bookmarkable):
) )
authors = models.ManyToManyField( authors = models.ManyToManyField(
Profile, Profile,
verbose_name=_("authors"),
help_text=_("These users will be able to edit the contest."), help_text=_("These users will be able to edit the contest."),
related_name="authors+", related_name="authors+",
) )
curators = models.ManyToManyField( curators = models.ManyToManyField(
Profile, Profile,
verbose_name=_("curators"),
help_text=_( help_text=_(
"These users will be able to edit the contest, " "These users will be able to edit the contest, "
"but will not be listed as authors." "but will not be listed as authors."
@ -113,6 +115,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
) )
testers = models.ManyToManyField( testers = models.ManyToManyField(
Profile, Profile,
verbose_name=_("testers"),
help_text=_( help_text=_(
"These users will be able to view the contest, " "but not edit it." "These users will be able to view the contest, " "but not edit it."
), ),

View file

@ -1,6 +1,5 @@
import errno import errno
from operator import attrgetter from operator import attrgetter
from math import sqrt
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
@ -557,7 +556,7 @@ class Problem(models.Model, PageVotable, Bookmarkable):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(Problem, self).save(*args, **kwargs) super(Problem, self).save(*args, **kwargs)
if self.code != self.__original_code: if self.__original_code and self.code != self.__original_code:
if hasattr(self, "data_files") or self.pdf_description: if hasattr(self, "data_files") or self.pdf_description:
try: try:
problem_data_storage.rename(self.__original_code, self.code) problem_data_storage.rename(self.__original_code, self.code)

View file

@ -162,10 +162,10 @@ class ProblemData(models.Model):
get_file_cachekey(file), get_file_cachekey(file),
) )
cache.delete(cache_key) cache.delete(cache_key)
except BadZipFile: except (BadZipFile, FileNotFoundError):
pass pass
if self.zipfile != self.__original_zipfile and self.__original_zipfile: if self.zipfile != self.__original_zipfile:
self.__original_zipfile.delete(save=False) self.__original_zipfile.delete(save=False)
return super(ProblemData, self).save(*args, **kwargs) return super(ProblemData, self).save(*args, **kwargs)
def has_yml(self): def has_yml(self):

View file

@ -1,5 +1,4 @@
import csv import csv
from tempfile import mktemp
import re import re
from django.conf import settings from django.conf import settings

View file

@ -5,7 +5,6 @@ from django import forms
from django.forms import ClearableFileInput from django.forms import ClearableFileInput
import os, os.path import os, os.path
import tempfile
import shutil import shutil
__all__ = ("handle_upload", "save_upload", "FineUploadForm", "FineUploadFileInput") __all__ = ("handle_upload", "save_upload", "FineUploadForm", "FineUploadFileInput")

View file

@ -2,7 +2,6 @@ import hashlib
import json import json
import os import os
import re import re
import shutil
import yaml import yaml
import zipfile import zipfile
@ -13,6 +12,8 @@ from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.core.cache import cache from django.core.cache import cache
from judge.logging import log_exception
if os.altsep: if os.altsep:
def split_path_first( def split_path_first(
@ -324,11 +325,13 @@ def get_problem_case(problem, files):
settings.DMOJ_PROBLEM_DATA_ROOT, str(problem.data_files.zipfile) settings.DMOJ_PROBLEM_DATA_ROOT, str(problem.data_files.zipfile)
) )
if not os.path.exists(archive_path): if not os.path.exists(archive_path):
raise Exception('archive file "%s" does not exist' % archive_path) log_exception('archive file "%s" does not exist' % archive_path)
return {}
try: try:
archive = zipfile.ZipFile(archive_path, "r") archive = zipfile.ZipFile(archive_path, "r")
except zipfile.BadZipfile: except zipfile.BadZipfile:
raise Exception('bad archive: "%s"' % archive_path) log_exception('bad archive: "%s"' % archive_path)
return {}
for file in uncached_files: 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))

View file

@ -1,8 +1,8 @@
from collections import defaultdict from collections import defaultdict
from math import e from math import e
import os, zipfile from datetime import datetime, timedelta
from datetime import datetime
import random import random
from enum import Enum
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -10,6 +10,7 @@ from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When
from django.db.models.fields import FloatField from django.db.models.fields import FloatField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _, gettext_noop from django.utils.translation import gettext as _, gettext_noop
from django.http import Http404
from judge.models import Problem, Submission from judge.models import Problem, Submission
from judge.ml.collab_filter import CollabFilter from judge.ml.collab_filter import CollabFilter
@ -249,3 +250,72 @@ def finished_submission(sub):
keys += ["contest_complete:%d" % participation.id] keys += ["contest_complete:%d" % participation.id]
keys += ["contest_attempted:%d" % participation.id] keys += ["contest_attempted:%d" % participation.id]
cache.delete_many(keys) cache.delete_many(keys)
class RecommendationType(Enum):
HOT_PROBLEM = 1
CF_DOT = 2
CF_COSINE = 3
CF_TIME_DOT = 4
CF_TIME_COSINE = 5
# Return a list of list. Each inner list correspond to each type in types
def get_user_recommended_problems(
user_id,
problem_ids,
recommendation_types,
limits,
shuffle=False,
):
cf_model = CollabFilter("collab_filter")
cf_time_model = CollabFilter("collab_filter_time")
def get_problem_ids_from_type(rec_type, limit):
if type(rec_type) == int:
try:
rec_type = RecommendationType(rec_type)
except ValueError:
raise Http404()
if rec_type == RecommendationType.HOT_PROBLEM:
return [
problem.id
for problem in hot_problems(timedelta(days=7), limit)
if problem.id in set(problem_ids)
]
if rec_type == RecommendationType.CF_DOT:
return cf_model.user_recommendations(
user_id, problem_ids, cf_model.DOT, limit
)
if rec_type == RecommendationType.CF_COSINE:
return cf_model.user_recommendations(
user_id, problem_ids, cf_model.COSINE, limit
)
if rec_type == RecommendationType.CF_TIME_DOT:
return cf_time_model.user_recommendations(
user_id, problem_ids, cf_model.DOT, limit
)
if rec_type == RecommendationType.CF_TIME_COSINE:
return cf_time_model.user_recommendations(
user_id, problem_ids, cf_model.COSINE, limit
)
return []
all_problems = []
for rec_type, limit in zip(recommendation_types, limits):
all_problems += get_problem_ids_from_type(rec_type, limit)
if shuffle:
seed = datetime.now().strftime("%d%m%Y")
random.Random(seed).shuffle(all_problems)
# deduplicate problems
res = []
used_pid = set()
for obj in all_problems:
if type(obj) == tuple:
obj = obj[1]
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res

View file

@ -144,7 +144,11 @@ class ContestList(
context_object_name = "past_contests" context_object_name = "past_contests"
all_sorts = frozenset(("name", "user_count", "start_time")) all_sorts = frozenset(("name", "user_count", "start_time"))
default_desc = frozenset(("name", "user_count")) default_desc = frozenset(("name", "user_count"))
default_sort = "-start_time"
def get_default_sort_order(self, request):
if request.GET.get("contest") and settings.ENABLE_FTS:
return "-relevance"
return "-start_time"
@cached_property @cached_property
def _now(self): def _now(self):
@ -157,7 +161,7 @@ class ContestList(
if request.GET.get("show_orgs"): if request.GET.get("show_orgs"):
self.show_orgs = 1 self.show_orgs = 1
if "orgs" in self.request.GET and self.request.profile: if self.request.GET.get("orgs") and self.request.profile:
try: try:
self.org_query = list(map(int, request.GET.getlist("orgs"))) self.org_query = list(map(int, request.GET.getlist("orgs")))
if not self.request.user.is_superuser: if not self.request.user.is_superuser:
@ -165,8 +169,10 @@ class ContestList(
i i
for i in self.org_query for i in self.org_query
if i if i
in self.request.profile.organizations.values_list( in set(
"id", flat=True self.request.profile.organizations.values_list(
"id", flat=True
)
) )
] ]
except ValueError: except ValueError:
@ -181,7 +187,7 @@ class ContestList(
.prefetch_related("tags", "organizations", "authors", "curators", "testers") .prefetch_related("tags", "organizations", "authors", "curators", "testers")
) )
if "contest" in self.request.GET: if self.request.GET.get("contest"):
self.contest_query = query = " ".join( self.contest_query = query = " ".join(
self.request.GET.getlist("contest") self.request.GET.getlist("contest")
).strip() ).strip()
@ -249,10 +255,7 @@ class ContestList(
context["org_query"] = self.org_query context["org_query"] = self.org_query
context["show_orgs"] = int(self.show_orgs) context["show_orgs"] = int(self.show_orgs)
if self.request.profile: if self.request.profile:
if self.request.user.is_superuser: context["organizations"] = self.request.profile.organizations.all()
context["organizations"] = Organization.objects.all()
else:
context["organizations"] = self.request.profile.organizations.all()
context["page_type"] = "list" context["page_type"] = "list"
context.update(self.get_sort_context()) context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context()) context.update(self.get_sort_paginate_context())
@ -419,7 +422,14 @@ class ContestDetail(
return [] return []
res = [] res = []
for organization in self.object.organizations.all(): for organization in self.object.organizations.all():
can_edit = False
if self.request.profile.can_edit_organization(organization): if self.request.profile.can_edit_organization(organization):
can_edit = True
if self.request.profile in organization and self.object.is_editable_by(
self.request.user
):
can_edit = True
if can_edit:
res.append(organization) res.append(organization)
return res return res
@ -441,16 +451,32 @@ class ContestDetail(
.add_i18n_name(self.request.LANGUAGE_CODE) .add_i18n_name(self.request.LANGUAGE_CODE)
) )
context["editable_organizations"] = self.get_editable_organizations() context["editable_organizations"] = self.get_editable_organizations()
context["is_clonable"] = is_contest_clonable(self.request, self.object)
return context return context
class ContestClone( def is_contest_clonable(request, contest):
ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView if not request.profile:
): return False
if not Organization.objects.filter(admins=request.profile).exists():
return False
if request.user.has_perm("judge.clone_contest"):
return True
if contest.ended:
return True
return False
class ContestClone(ContestMixin, TitleMixin, SingleObjectFormView):
title = _("Clone Contest") title = _("Clone Contest")
template_name = "contest/clone.html" template_name = "contest/clone.html"
form_class = ContestCloneForm form_class = ContestCloneForm
permission_required = "judge.clone_contest"
def get_object(self, queryset=None):
contest = super().get_object(queryset)
if not is_contest_clonable(self.request, contest):
raise Http404()
return contest
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
@ -475,6 +501,7 @@ class ContestClone(
contest.is_visible = False contest.is_visible = False
contest.user_count = 0 contest.user_count = 0
contest.key = form.cleaned_data["key"] contest.key = form.cleaned_data["key"]
contest.is_rated = False
contest.save() contest.save()
contest.tags.set(tags) contest.tags.set(tags)

View file

@ -6,6 +6,7 @@ from django.utils.translation import gettext as _, gettext_lazy
from django.db.models import Count, Q from django.db.models import Count, Q
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.urls import reverse from django.urls import reverse
from django.shortcuts import render
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
from judge.models import VolunteerProblemVote, Problem from judge.models import VolunteerProblemVote, Problem
@ -21,7 +22,7 @@ class InternalView(object):
class InternalProblem(InternalView, ListView): class InternalProblem(InternalView, ListView):
model = Problem model = Problem
title = _("Internal problems") title = _("Internal problems")
template_name = "internal/problem.html" template_name = "internal/problem/problem.html"
paginate_by = 100 paginate_by = 100
context_object_name = "problems" context_object_name = "problems"
@ -63,6 +64,28 @@ class InternalProblem(InternalView, ListView):
return context return context
def get_problem_votes(request):
if not request.user.is_superuser:
return HttpResponseForbidden()
try:
problem = Problem.objects.get(id=request.GET.get("id"))
except:
return HttpResponseForbidden()
votes = (
problem.volunteer_user_votes.select_related("voter")
.prefetch_related("types")
.order_by("id")
)
return render(
request,
"internal/problem/votes.html",
{
"problem": problem,
"votes": votes,
},
)
class RequestTimeMixin(object): class RequestTimeMixin(object):
def get_requests_data(self): def get_requests_data(self):
logger = logging.getLogger(self.log_name) logger = logging.getLogger(self.log_name)

View file

@ -73,6 +73,7 @@ from judge.views.problem import ProblemList
from judge.views.contests import ContestList from judge.views.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.feed import FeedView from judge.views.feed import FeedView
from judge.tasks import rescore_contest
__all__ = [ __all__ = [
"OrganizationList", "OrganizationList",
@ -394,7 +395,7 @@ class OrganizationContestMixin(
model = Contest model = Contest
def is_contest_editable(self, request, contest): def is_contest_editable(self, request, contest):
return request.profile in contest.authors.all() or self.can_edit_organization( return contest.is_editable_by(request.user) or self.can_edit_organization(
self.organization self.organization
) )
@ -947,7 +948,7 @@ class EditOrganizationContest(
def get_content_title(self): def get_content_title(self):
href = reverse("contest_view", args=[self.contest.key]) href = reverse("contest_view", args=[self.contest.key])
return mark_safe(f'Edit <a href="{href}">{self.contest.key}</a>') return mark_safe(_("Edit") + f' <a href="{href}">{self.contest.key}</a>')
def get_object(self): def get_object(self):
return self.contest return self.contest
@ -960,6 +961,19 @@ class EditOrganizationContest(
self.object.organizations.add(self.organization) self.object.organizations.add(self.organization)
self.object.is_organization_private = True self.object.is_organization_private = True
self.object.save() self.object.save()
if any(
f in form.changed_data
for f in (
"start_time",
"end_time",
"time_limit",
"format_config",
"format_name",
"freeze_after",
)
):
transaction.on_commit(rescore_contest.s(self.object.key).delay)
return res return res
def get_problem_formset(self, post=False): def get_problem_formset(self, post=False):

View file

@ -1,10 +1,8 @@
import logging import logging
import os import os
import shutil import shutil
from datetime import timedelta, datetime
from operator import itemgetter from operator import itemgetter
from random import randrange from random import randrange
import random
from copy import deepcopy from copy import deepcopy
from django.core.cache import cache from django.core.cache import cache
@ -77,6 +75,8 @@ from judge.utils.problems import (
user_attempted_ids, user_attempted_ids,
user_completed_ids, user_completed_ids,
get_related_problems, get_related_problems,
get_user_recommended_problems,
RecommendationType,
) )
from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter from judge.utils.tickets import own_ticket_filter
@ -834,24 +834,34 @@ class ProblemFeed(ProblemList, FeedView):
title = _("Problem feed") title = _("Problem feed")
feed_type = None feed_type = None
# arr = [[], [], ..] def get_recommended_problem_ids(self, queryset):
def merge_recommendation(self, arr): user_id = self.request.profile.id
seed = datetime.now().strftime("%d%m%Y") problem_ids = queryset.values_list("id", flat=True)
merged_array = [] rec_types = [
for a in arr: RecommendationType.CF_DOT,
merged_array += a RecommendationType.CF_COSINE,
random.Random(seed).shuffle(merged_array) RecommendationType.CF_TIME_DOT,
RecommendationType.CF_TIME_COSINE,
RecommendationType.HOT_PROBLEM,
]
limits = [100, 100, 100, 100, 20]
shuffle = True
res = [] allow_debug_type = (
used_pid = set() self.request.user.is_impersonate or self.request.user.is_superuser
)
if allow_debug_type and "debug_type" in self.request.GET:
try:
debug_type = int(self.request.GET.get("debug_type"))
except ValueError:
raise Http404()
rec_types = [debug_type]
limits = [100]
shuffle = False
for obj in merged_array: return get_user_recommended_problems(
if type(obj) == tuple: user_id, problem_ids, rec_types, limits, shuffle
obj = obj[1] )
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res
def get_queryset(self): def get_queryset(self):
if self.feed_type == "volunteer": if self.feed_type == "volunteer":
@ -885,40 +895,8 @@ class ProblemFeed(ProblemList, FeedView):
if not settings.ML_OUTPUT_PATH or not user: if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE) return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
cf_model = CollabFilter("collab_filter") q = self.get_recommended_problem_ids(queryset)
cf_time_model = CollabFilter("collab_filter_time")
queryset = queryset.values_list("id", flat=True)
hot_problems_recommendations = [
problem.id
for problem in hot_problems(timedelta(days=7), 20)
if problem.id in set(queryset)
]
q = self.merge_recommendation(
[
cf_model.user_recommendations(user, queryset, cf_model.DOT, 100),
cf_model.user_recommendations(
user,
queryset,
cf_model.COSINE,
100,
),
cf_time_model.user_recommendations(
user,
queryset,
cf_time_model.COSINE,
100,
),
cf_time_model.user_recommendations(
user,
queryset,
cf_time_model.DOT,
100,
),
hot_problems_recommendations,
]
)
queryset = Problem.objects.filter(id__in=q) queryset = Problem.objects.filter(id__in=q)
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)

View file

@ -56,6 +56,7 @@ from judge.utils.fine_uploader import (
FineUploadForm, FineUploadForm,
) )
from judge.views.problem import ProblemMixin from judge.views.problem import ProblemMixin
from judge.logging import log_exception
mimetypes.init() mimetypes.init()
mimetypes.add_type("application/x-yaml", ".yml") mimetypes.add_type("application/x-yaml", ".yml")
@ -249,6 +250,9 @@ class ProblemDataView(TitleMixin, ProblemManagerMixin):
return ZipFile(data.zipfile.path).namelist() return ZipFile(data.zipfile.path).namelist()
except BadZipfile: except BadZipfile:
return [] return []
except FileNotFoundError as e:
log_exception(e)
return []
return [] return []
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View file

@ -1,6 +1,6 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden, JsonResponse
from judge.models import Contest from judge.models import Contest
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -21,7 +21,7 @@ class Resolver(TemplateView):
hidden_subtasks = self.contest.format.get_hidden_subtasks() hidden_subtasks = self.contest.format.get_hidden_subtasks()
num_problems = len(problems) num_problems = len(problems)
problem_sub = [0] * num_problems problem_sub = [0] * num_problems
sub_frozen = [0] * num_problems sub_frozen = [[] for _ in range(num_problems)]
problems_json = {str(i): {} for i in range(1, num_problems + 1)} problems_json = {str(i): {} for i in range(1, num_problems + 1)}
users = {} users = {}
@ -126,10 +126,8 @@ class Resolver(TemplateView):
for i in hidden_subtasks: for i in hidden_subtasks:
order = id_to_order[i] order = id_to_order[i]
if hidden_subtasks[i]: sub_frozen[order - 1] = list(hidden_subtasks[i])
sub_frozen[order - 1] = min(hidden_subtasks[i])
else:
sub_frozen[order - 1] = problem_sub[order - 1] + 1
return { return {
"problem_sub": problem_sub, "problem_sub": problem_sub,
"sub_frozen": sub_frozen, "sub_frozen": sub_frozen,
@ -143,8 +141,15 @@ class Resolver(TemplateView):
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.user.is_superuser: if not request.user.is_superuser:
self.contest = Contest.objects.get(key=kwargs.get("contest")) return HttpResponseForbidden()
if self.contest.format.has_hidden_subtasks: self.contest = Contest.objects.get(key=kwargs.get("contest"))
return super(Resolver, self).get(request, *args, **kwargs) if not self.contest.format.has_hidden_subtasks:
return HttpResponseForbidden() return HttpResponseForbidden()
if self.request.GET.get("json"):
json_dumps_params = {"ensure_ascii": False}
return JsonResponse(
self.get_contest_json(), json_dumps_params=json_dumps_params
)
return super(Resolver, self).get(request, *args, **kwargs)

View file

@ -1,6 +1,5 @@
import json import json
import os.path import os.path
import zipfile
from operator import attrgetter from operator import attrgetter
from django.conf import settings from django.conf import settings
@ -196,8 +195,8 @@ def get_cases_data(submission):
continue continue
count += 1 count += 1
problem_data[count] = { problem_data[count] = {
"input": case_data[case.input_file] if case.input_file else "", "input": case_data.get(case.input_file, "") if case.input_file else "",
"answer": case_data[case.output_file] if case.output_file else "", "answer": case_data.get(case.output_file, "") if case.output_file else "",
} }
return problem_data return problem_data

File diff suppressed because it is too large Load diff

View file

@ -39,12 +39,6 @@ msgstr "Đăng ký tên"
msgid "Report" msgid "Report"
msgstr "Báo cáo" msgstr "Báo cáo"
msgid "Insert Image"
msgstr "Chèn hình ảnh"
msgid "Save"
msgstr "Lưu"
msgid "2sat" msgid "2sat"
msgstr "" msgstr ""
@ -600,3 +594,8 @@ msgstr ""
msgid "z-function" msgid "z-function"
msgstr "" msgstr ""
#~ msgid "Insert Image"
#~ msgstr "Chèn hình ảnh"
#~ msgid "Save"
#~ msgstr "Lưu"

View file

@ -147,8 +147,9 @@
} }
} }
.blog-box:hover { .blog-box:hover, .blog-box:not(.pre-expand-blog) {
border-color: #8a8a8a; border-color: #8a8a8a;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
} }
.blog-description { .blog-description {

View file

@ -269,10 +269,10 @@ a {
.actionbar-button { .actionbar-button {
cursor: pointer; cursor: pointer;
padding: 0.8em; padding: 0.8em;
border: 0.2px solid lightgray;
border-radius: 5em; border-radius: 5em;
font-weight: bold; font-weight: bold;
display: inherit; display: inherit;
background: lightgray;
} }
.actionbar-block { .actionbar-block {
display: flex; display: flex;
@ -287,7 +287,7 @@ a {
border-radius: 5em 0 0 5em; border-radius: 5em 0 0 5em;
} }
.actionbar-button:hover { .actionbar-button:hover {
background: lightgray; background: darkgray;
} }
.dislike-button { .dislike-button {
padding-left: 0.5em; padding-left: 0.5em;

View file

@ -42,7 +42,6 @@
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
margin: 1.5em 0 1.5em 0;
padding: 1em; padding: 1em;
border: 1px solid $border_gray; border: 1px solid $border_gray;
background-color: #f8f8f8; background-color: #f8f8f8;

View file

@ -1956,8 +1956,10 @@ input::placeholder {
background-color: rgb(24, 26, 27); background-color: rgb(24, 26, 27);
box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 5px; box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 5px;
} }
.blog-box:hover { .blog-box:hover,
.blog-box:not(.pre-expand-blog) {
border-color: rgb(81, 88, 91); border-color: rgb(81, 88, 91);
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 2px;
} }
.problem-feed-name a { .problem-feed-name a {
color: rgb(102, 177, 250); color: rgb(102, 177, 250);
@ -2091,6 +2093,9 @@ ul.problem-list {
#comment-announcement:hover { #comment-announcement:hover {
background-color: rgb(96, 104, 108); background-color: rgb(96, 104, 108);
} }
.new-problem-info {
background-color: rgb(54, 39, 0);
}
.admin a, .admin a,
.admin { .admin {
color: rgb(232, 230, 227) !important; color: rgb(232, 230, 227) !important;
@ -2780,11 +2785,12 @@ a.voted {
border-left-color: rgb(48, 52, 54); border-left-color: rgb(48, 52, 54);
} }
.actionbar .actionbar-button { .actionbar .actionbar-button {
border-color: rgb(60, 65, 68); background-image: initial;
background-color: rgb(49, 53, 55);
} }
.actionbar .actionbar-button:hover { .actionbar .actionbar-button:hover {
background-image: initial; background-image: initial;
background-color: rgb(49, 53, 55); background-color: rgb(73, 79, 82);
} }
.actionbar .dislike-button { .actionbar .dislike-button {
border-left-color: initial; border-left-color: initial;

View file

@ -25,17 +25,17 @@ div.dmmd-preview-has-content div.dmmd-preview-content {
} }
div.dmmd-no-button div.dmmd-preview-update { div.dmmd-no-button div.dmmd-preview-update {
display: none; display: none;
} }
div.dmmd-no-button div.dmmd-preview-content { div.dmmd-no-button div.dmmd-preview-content {
padding-bottom: 0; padding-bottom: 0;
} }
div.dmmd-no-button:not(.dmmd-preview-has-content) { div.dmmd-no-button:not(.dmmd-preview-has-content) {
display: none; display: none;
} }
div.dmmd-preview-stale { div.dmmd-preview-stale {
background: repeating-linear-gradient(-45deg, #fff, #fff 10px, #f8f8f8 10px, #f8f8f8 20px); background: repeating-linear-gradient(-45deg, #fff, #fff 10px, #f8f8f8 10px, #f8f8f8 20px);
} }

View file

@ -4,59 +4,59 @@
if you are not yet familiar with Fine Uploader UI. if you are not yet familiar with Fine Uploader UI.
--> -->
<script type="text/template" id="qq-template"> <script type="text/template" id="qq-template">
<div class="qq-uploader-selector qq-uploader" qq-drop-area-text="Drop files here"> <div class="qq-uploader-selector qq-uploader" qq-drop-area-text="Drop files here">
<div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container"> <div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div> <div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div>
</div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" aria-live="polite" aria-relevant="additions removals">
<li>
<div class="qq-progress-bar-container-selector">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"></span>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-cancel-selector qq-upload-cancel">Cancel</button>
<button type="button" class="qq-btn qq-upload-retry-selector qq-upload-retry">Retry</button>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">Delete</button>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div> </div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" aria-live="polite" aria-relevant="additions removals">
<li>
<div class="qq-progress-bar-container-selector">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"></span>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-cancel-selector qq-upload-cancel">Cancel</button>
<button type="button" class="qq-btn qq-upload-retry-selector qq-upload-retry">Retry</button>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">Delete</button>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div>
</script> </script>

View file

@ -5,78 +5,78 @@
on how to customize this template. on how to customize this template.
--> -->
<script type="text/template" id="qq-template"> <script type="text/template" id="qq-template">
<div class="qq-uploader-selector qq-uploader qq-gallery" qq-drop-area-text="Drop files here"> <div class="qq-uploader-selector qq-uploader qq-gallery" qq-drop-area-text="Drop files here">
<div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container"> <div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div> <div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div>
</div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" role="region" aria-live="polite" aria-relevant="additions removals">
<li>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
<div class="qq-progress-bar-container-selector qq-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<div class="qq-thumbnail-wrapper">
<img class="qq-thumbnail-selector" qq-max-size="120" qq-server-scale>
</div>
<button type="button" class="qq-upload-cancel-selector qq-upload-cancel">X</button>
<button type="button" class="qq-upload-retry-selector qq-upload-retry">
<span class="qq-btn qq-retry-icon" aria-label="Retry"></span>
Retry
</button>
<div class="qq-file-info">
<div class="qq-file-name">
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-btn qq-edit-filename-icon" aria-label="Edit filename"></span>
</div>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">
<span class="qq-btn qq-delete-icon" aria-label="Delete"></span>
</button>
<button type="button" class="qq-btn qq-upload-pause-selector qq-upload-pause">
<span class="qq-btn qq-pause-icon" aria-label="Pause"></span>
</button>
<button type="button" class="qq-btn qq-upload-continue-selector qq-upload-continue">
<span class="qq-btn qq-continue-icon" aria-label="Continue"></span>
</button>
</div>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div> </div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" role="region" aria-live="polite" aria-relevant="additions removals">
<li>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
<div class="qq-progress-bar-container-selector qq-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<div class="qq-thumbnail-wrapper">
<img class="qq-thumbnail-selector" qq-max-size="120" qq-server-scale>
</div>
<button type="button" class="qq-upload-cancel-selector qq-upload-cancel">X</button>
<button type="button" class="qq-upload-retry-selector qq-upload-retry">
<span class="qq-btn qq-retry-icon" aria-label="Retry"></span>
Retry
</button>
<div class="qq-file-info">
<div class="qq-file-name">
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-btn qq-edit-filename-icon" aria-label="Edit filename"></span>
</div>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">
<span class="qq-btn qq-delete-icon" aria-label="Delete"></span>
</button>
<button type="button" class="qq-btn qq-upload-pause-selector qq-upload-pause">
<span class="qq-btn qq-pause-icon" aria-label="Pause"></span>
</button>
<button type="button" class="qq-btn qq-upload-continue-selector qq-upload-continue">
<span class="qq-btn qq-continue-icon" aria-label="Continue"></span>
</button>
</div>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div>
</script> </script>

View file

@ -5,60 +5,60 @@
on how to customize this template. on how to customize this template.
--> -->
<script type="text/template" id="qq-simple-thumbnails-template"> <script type="text/template" id="qq-simple-thumbnails-template">
<div class="qq-uploader-selector qq-uploader" qq-drop-area-text="Drop files here"> <div class="qq-uploader-selector qq-uploader" qq-drop-area-text="Drop files here">
<div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container"> <div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div> <div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div>
</div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" aria-live="polite" aria-relevant="additions removals">
<li>
<div class="qq-progress-bar-container-selector">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<img class="qq-thumbnail-selector" qq-max-size="100" qq-server-scale>
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"></span>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-cancel-selector qq-upload-cancel">Cancel</button>
<button type="button" class="qq-btn qq-upload-retry-selector qq-upload-retry">Retry</button>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">Delete</button>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div> </div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" aria-live="polite" aria-relevant="additions removals">
<li>
<div class="qq-progress-bar-container-selector">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<img class="qq-thumbnail-selector" qq-max-size="100" qq-server-scale>
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"></span>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-cancel-selector qq-upload-cancel">Cancel</button>
<button type="button" class="qq-btn qq-upload-retry-selector qq-upload-retry">Retry</button>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">Delete</button>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div>
</script> </script>

View file

@ -1,49 +1,49 @@
.mwe-math-mathml-inline { .mwe-math-mathml-inline {
display: inline !important; display: inline !important;
} }
.mwe-math-mathml-display { .mwe-math-mathml-display {
display: block !important; display: block !important;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.mwe-math-mathml-a11y { .mwe-math-mathml-a11y {
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
overflow: hidden; overflow: hidden;
position: absolute; position: absolute;
width: 1px; width: 1px;
height: 1px; height: 1px;
opacity: 0; opacity: 0;
} }
.mwe-math-fallback-image-inline { .mwe-math-fallback-image-inline {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
.mwe-math-fallback-image-display { .mwe-math-fallback-image-display {
display: block; display: block;
margin-left: auto !important; margin-left: auto !important;
margin-right: auto !important; margin-right: auto !important;
} }
@font-face { @font-face {
font-family: 'Latin Modern Math'; font-family: 'Latin Modern Math';
src: url('libs/latinmodernmath/latinmodern-math.eot'); /* IE9 Compat Modes */ src: url('libs/latinmodernmath/latinmodern-math.eot'); /* IE9 Compat Modes */
src: local('Latin Modern Math'), local('LatinModernMath-Regular'), src: local('Latin Modern Math'), local('LatinModernMath-Regular'),
url('libs/latinmodernmath/latinmodern-math.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('libs/latinmodernmath/latinmodern-math.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('libs/latinmodernmath/latinmodern-math.woff2') format('woff2'), /* Modern Browsers */ url('libs/latinmodernmath/latinmodern-math.woff2') format('woff2'), /* Modern Browsers */
url('libs/latinmodernmath/latinmodern-math.woff') format('woff'), /* Modern Browsers */ url('libs/latinmodernmath/latinmodern-math.woff') format('woff'), /* Modern Browsers */
url('libs/latinmodernmath/latinmodern-math.ttf') format('truetype'); /* Safari, Android, iOS */ url('libs/latinmodernmath/latinmodern-math.ttf') format('truetype'); /* Safari, Android, iOS */
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
math { math {
font-family: "Latin Modern Math"; font-family: "Latin Modern Math";
} }
img.inline-math { img.inline-math {
display: inline; display: inline;
} }

View file

@ -416,3 +416,19 @@ ul.problem-list {
} }
} }
} }
.new-problem-info {
background-color: #fff6dd;
border-radius: 25px;
font-size: 14px;
height: 25px;
width: 100%;
display: table;
padding: 5px;
margin-top: 12px;
}
.info-block {
display:table-cell;
vertical-align:middle;
}

View file

@ -50,7 +50,7 @@ th.header.rank {
th a:hover { th a:hover {
color: #0F0; color: #0F0;
} }
.about-column { .about-column {
width: 30%; width: 30%;
} }
@ -362,7 +362,7 @@ a.edit-profile {
} }
.follow { .follow {
background: green; background: green;
border-color: lightgreen; border-color: lightgreen;
} }
.follow:hover { .follow:hover {

View file

@ -113,7 +113,7 @@
navigator.clipboard navigator.clipboard
.writeText(link) .writeText(link)
.then(() => { .then(() => {
showTooltip(element, "Copied link", 'n'); showTooltip(element, "{{_('Copied link')}}", 'n');
}); });
}; };

View file

@ -75,112 +75,6 @@
<link rel="stylesheet" href="{{ static('darkmode-svg.css') }}"> <link rel="stylesheet" href="{{ static('darkmode-svg.css') }}">
{% endcompress %} {% endcompress %}
{% endif %} {% endif %}
{% if not INLINE_JQUERY %}
<script src="{{ JQUERY_JS }}"></script>
{% endif %}
<script src="https://unpkg.com/@popperjs/core@2"></script>
{% compress js %}
<script>{{ inlinei18n(LANGUAGE_CODE)|safe }}</script>
{% if INLINE_JQUERY %}
<script src="{{ static('libs/jquery-3.4.1.min.js') }}"></script>
{% endif %}
<script src="{{ static('libs/jquery-cookie.js') }}"></script>
<script src="{{ static('libs/jquery-taphold.js') }}"></script>
<script src="{{ static('libs/jquery.unveil.js') }}"></script>
<script src="{{ static('libs/moment.js') }}"></script>
<script src="{{ static('libs/select2/select2.js') }}"></script>
<script src="{{ static('libs/clipboard/clipboard.js') }}"></script>
{% include "extra_js.html" %}
<script src="{{ static('common.js') }}"></script>
<script src="{{ static('libs/clipboard/tooltip.js') }}"></script>
<script>
moment.locale('{{ LANGUAGE_CODE }}');
$(function () {
$('img.unveil').unveil(200);
});
const loading_page = `{% include "loading-page.html" %}`;
</script>
{% endcompress %}
{% block js_media %}{% endblock %}
{% if request.in_contest %}
<script>$(function () {
if ($("#contest-time-remaining").length) {
count_down($("#contest-time-remaining"));
}
var selected = null,
x_pos = 0, y_pos = 0,
x_elem = 0, y_elem = 0;
$('#contest-info').mousedown(function () {
selected = $(this);
x_elem = x_pos - selected.offset().left;
y_elem = y_pos - (selected.offset().top - $(window).scrollTop());
return false;
});
if (localStorage.getItem("contest_timer_position")) {
data = localStorage.getItem("contest_timer_position").split(":");
$("#contest-info").css({
left: data[0],
top: data[1]
});
}
$("#contest-info").show();
$("#contest-info-toggle").on('click', function() {
$.post("{{url('contest_mode_ajax')}}", function() {
window.location.reload();
})
});
$(document).mousemove(function (e) {
x_pos = e.screenX;
y_pos = e.screenY;
if (selected !== null) {
left_px = (x_pos - x_elem);
top_px = (y_pos - y_elem);
left_px = Math.max(Math.min(left_px, window.innerWidth), 0) / window.innerWidth * 100 + '%';
top_px = Math.max(Math.min(top_px, window.innerHeight), 0) / window.innerHeight * 100 + '%';
localStorage.setItem("contest_timer_position", left_px + ":" + top_px);
selected.css({
left: left_px,
top: top_px
});
}
});
$(document).mouseup(function () {
selected = null;
})
});
</script>
{% endif %}
{% if request.user.is_authenticated %}
<script>
window.user = {
email: '{{ request.user.email|escapejs }}',
id: '{{ request.user.id|escapejs }}',
name: '{{ request.user.username|escapejs }}'
};
</script>
{% else %}
<script>window.user = {};</script>
{% endif %}
{% if misc_config.analytics %}
{{ misc_config.analytics|safe }}
{% endif %}
{# Don't run userscript since it may be malicious #}
{% if request.user.is_authenticated and request.profile.user_script and not request.user.is_impersonate %}
<script type="text/javascript">{{ request.profile.user_script|safe }}</script>
{% endif %}
<noscript> <noscript>
<style> <style>
@ -378,12 +272,121 @@
<div id="announcement">{{ i18n_config.announcement|safe }}</div> <div id="announcement">{{ i18n_config.announcement|safe }}</div>
{% endif %} {% endif %}
{% if not INLINE_JQUERY %}
<script src="{{ JQUERY_JS }}"></script>
{% endif %}
<script src="https://unpkg.com/@popperjs/core@2"></script>
{% compress js %}
<script>{{ inlinei18n(LANGUAGE_CODE)|safe }}</script>
{% if INLINE_JQUERY %}
<script src="{{ static('libs/jquery-3.4.1.min.js') }}"></script>
{% endif %}
<script src="{{ static('libs/jquery-cookie.js') }}"></script>
<script src="{{ static('libs/jquery-taphold.js') }}"></script>
<script src="{{ static('libs/jquery.unveil.js') }}"></script>
<script src="{{ static('libs/moment.js') }}"></script>
<script src="{{ static('libs/select2/select2.js') }}"></script>
<script src="{{ static('libs/clipboard/clipboard.js') }}"></script>
{% include "extra_js.html" %}
<script src="{{ static('common.js') }}"></script>
<script src="{{ static('libs/clipboard/tooltip.js') }}"></script>
<script>
moment.locale('{{ LANGUAGE_CODE }}');
$(function () {
$('img.unveil').unveil(200);
});
</script>
{% endcompress %}
{% block js_media %}{% endblock %}
{% if request.in_contest %}
<script>$(function () {
if ($("#contest-time-remaining").length) {
count_down($("#contest-time-remaining"));
}
var selected = null,
x_pos = 0, y_pos = 0,
x_elem = 0, y_elem = 0;
$('#contest-info').mousedown(function () {
selected = $(this);
x_elem = x_pos - selected.offset().left;
y_elem = y_pos - (selected.offset().top - $(window).scrollTop());
return false;
});
if (localStorage.getItem("contest_timer_position")) {
data = localStorage.getItem("contest_timer_position").split(":");
$("#contest-info").css({
left: data[0],
top: data[1]
});
}
$("#contest-info").show();
$("#contest-info-toggle").on('click', function() {
$.post("{{url('contest_mode_ajax')}}", function() {
window.location.reload();
})
});
$(document).mousemove(function (e) {
x_pos = e.screenX;
y_pos = e.screenY;
if (selected !== null) {
left_px = (x_pos - x_elem);
top_px = (y_pos - y_elem);
left_px = Math.max(Math.min(left_px, window.innerWidth), 0) / window.innerWidth * 100 + '%';
top_px = Math.max(Math.min(top_px, window.innerHeight), 0) / window.innerHeight * 100 + '%';
localStorage.setItem("contest_timer_position", left_px + ":" + top_px);
selected.css({
left: left_px,
top: top_px
});
}
});
$(document).mouseup(function () {
selected = null;
})
});
</script>
{% endif %}
{% if request.user.is_authenticated %}
<script>
window.user = {
email: '{{ request.user.email|escapejs }}',
id: '{{ request.user.id|escapejs }}',
name: '{{ request.user.username|escapejs }}'
};
</script>
{% else %}
<script>window.user = {};</script>
{% endif %}
{% if misc_config.analytics %}
{{ misc_config.analytics|safe }}
{% endif %}
{# Don't run userscript since it may be malicious #}
{% if request.user.is_authenticated and request.profile.user_script and not request.user.is_impersonate %}
<script type="text/javascript">{{ request.profile.user_script|safe }}</script>
{% endif %}
<div id="extra_js">
{% block extra_js %}{% endblock %}
</div>
{% block bodyend %}{% endblock %} {% block bodyend %}{% endblock %}
{% block footer %} {% block footer %}
<footer> <footer>
<span id="footer-content"> <span id="footer-content">
<br> <br>
<a class="background-footer" target="_blank" href="https://dmoj.ca">proudly powered by <b>DMOJ</b></a><a target="_blank" href="https://github.com/LQDJudge/online-judge"> | developed by LQDJudge team</a> | <a class="background-footer" target="_blank" href="https://dmoj.ca">proudly powered by <b>DMOJ</b></a>|<a target="_blank" href="https://github.com/LQDJudge/online-judge"> developed by LQDJudge team</a>
{% if i18n_config.footer %} {% if i18n_config.footer %}
{{ i18n_config.footer|safe }} | {{ i18n_config.footer|safe }} |
{% endif %} {% endif %}

View file

@ -5,7 +5,10 @@
{% block title %} {{_('Chat Box')}} {% endblock %} {% block title %} {{_('Chat Box')}} {% endblock %}
{% block js_media %} {% block js_media %}
<script type="text/javascript" src="{{ static('mathjax3_config.js') }}"></script> {% if REQUIRE_JAX %}
{% include "mathjax-load.html" %}
{% endif %}
{% include "comments/math.html" %}
<script type="text/javascript" src="{{ static('event.js') }}"></script> <script type="text/javascript" src="{{ static('event.js') }}"></script>
<script type="module" src="https://unpkg.com/emoji-picker-element@1"></script> <script type="module" src="https://unpkg.com/emoji-picker-element@1"></script>
{% compress js %} {% compress js %}
@ -79,7 +82,7 @@
{% include 'chat/user_online_status.html' %} {% include 'chat/user_online_status.html' %}
</div> </div>
<div id="chat-box"> <div id="chat-box">
<img src="{{static('loading.gif')}}" id="loader"> <img src="{{static('loading.gif')}}" id="loader" style="display: none;">
<ul id="chat-log"> <ul id="chat-log">
{% include 'chat/message_list.html' %} {% include 'chat/message_list.html' %}
</ul> </ul>

View file

@ -10,7 +10,15 @@
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 20px; width: 16px;
background-color: transparent !important;
}
#chat-input::-webkit-scrollbar {
width: 22px;
}
#chat-input::-webkit-scrollbar-thumb {
border: 10px solid transparent;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@ -144,9 +152,11 @@
transition: box-shadow 0.3s ease-in-out; transition: box-shadow 0.3s ease-in-out;
width: 80%; width: 80%;
resize: none; resize: none;
height: 80%; height: 70%;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
margin-top: auto;
margin-bottom: 6px;
} }
#chat-input:focus { #chat-input:focus {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);

View file

@ -1,10 +1,4 @@
<script type="text/javascript"> <script type="text/javascript">
let META_HEADER = [
"{{_('Recent')}}",
"{{_('Following')}}",
"{{_('Admin')}}",
"{{_('Other')}}",
];
let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches; let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches;
function load_next_page(last_id, refresh_html=false) { function load_next_page(last_id, refresh_html=false) {
@ -561,7 +555,7 @@
this.style.height = (this.scrollHeight) + 'px'; this.style.height = (this.scrollHeight) + 'px';
$(this).css('border-radius', '30px'); $(this).css('border-radius', '30px');
} else { } else {
$(this).css('height', '80%'); $(this).css('height', '70%');
} }
}); });

View file

@ -1,12 +1,12 @@
<li class="message" id="message-{{ message.id }}" message-id="{{ message.id }}"> <li class="message" id="message-{{ message.id }}" message-id="{{ message.id }}">
<a href="{{ url('user_page', message.author.user.username) }}"> <a href="{{ url('user_page', message.author.username) }}">
<img src="{{ gravatar(message.author, 135) }}" class="profile-pic"> <img src="{{ gravatar(message.author, 135) }}" class="profile-pic">
</a> </a>
<div class="body-message"> <div class="body-message">
<div class="user-time"> <div class="user-time">
<span class="username {{ message.author.css_class }}"> <span class="username {{ message.author.css_class }}">
<a href="{{ url('user_page', message.author.user.username) }}"> <a href="{{ url('user_page', message.author.username) }}">
{{ message.author }} {{ message.author.username }}
</a> </a>
</span> </span>
<span class="time"> <span class="time">

View file

@ -6,8 +6,4 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<center id="empty_msg">{{_('You are connect now. Say something to start the conversation.')}}</center> <center id="empty_msg">{{_('You are connect now. Say something to start the conversation.')}}</center>
{% endif %} {% endif %}
{% if REQUIRE_JAX %}
{% include "mathjax-load.html" %}
{% endif %}
{% include "comments/math.html" %}

View file

@ -145,6 +145,8 @@
$comment_loading.hide(); $comment_loading.hide();
var $comment = $("#comment-" + id + "-children"); var $comment = $("#comment-" + id + "-children");
$comment.append(data); $comment.append(data);
MathJax.typeset($('#comments')[0]);
register_time($('.time-with-rel'));
} }
}) })
} }
@ -187,6 +189,7 @@
$comment.append(data); $comment.append(data);
} }
MathJax.typeset($('#comments')[0]); MathJax.typeset($('#comments')[0]);
register_time($('.time-with-rel'));
} }
}) })
} }

View file

@ -44,7 +44,7 @@
<div><label class="inline-header grayed">{{ _('Enter a new key for the cloned contest:') }}</label></div> <div><label class="inline-header grayed">{{ _('Enter a new key for the cloned contest:') }}</label></div>
<div id="contest-key-container"><span class="fullwidth">{{ form.key }}</span></div> <div id="contest-key-container"><span class="fullwidth">{{ form.key }}</span></div>
<div><label class="inline-header grayed">{{ _('Group:') }}</label></div> <div><label class="inline-header grayed">{{ _('Group') }}:</label></div>
{{form.organization}} {{form.organization}}
{% if form.organization.errors %} {% if form.organization.errors %}
<div id="form-errors"> <div id="form-errors">

View file

@ -36,7 +36,4 @@
{% endif %} {% endif %}
{{ make_tab_item('edit', 'fa fa-edit', url('admin:judge_contest_change', contest.id), _('Edit')) }} {{ make_tab_item('edit', 'fa fa-edit', url('admin:judge_contest_change', contest.id), _('Edit')) }}
{% endif %} {% endif %}
{% if perms.judge.clone_contest %}
{{ make_tab_item('clone', 'fa fa-copy', url('contest_clone', contest.key), _('Clone')) }}
{% endif %}
</div> </div>

View file

@ -82,11 +82,16 @@
{{ contest.description|markdown|reference|str|safe }} {{ contest.description|markdown|reference|str|safe }}
{% endcache %} {% endcache %}
</div> </div>
{% if editable_organizations %} {% if editable_organizations or is_clonable %}
<div> <div style="display: flex; gap: 0.5em;">
{% for org in editable_organizations %} {% for org in editable_organizations %}
<span> [<a href="{{ url('organization_contest_edit', org.id , org.slug , contest.key) }}">{{ _('Edit in') }} {{org.slug}}</a>]</span> <span> [<a href="{{ url('organization_contest_edit', org.id , org.slug , contest.key) }}">{{ _('Edit in') }} {{org.slug}}</a>]</span>
{% endfor %} {% endfor %}
{% if is_clonable %}
<span>
[<a href="{{url('contest_clone', contest.key)}}"}}>{{_('Clone')}}</a>]
</span>
{% endif %}
</div> </div>
{% endif %} {% endif %}

View file

@ -13,15 +13,15 @@
{% endblock %} {% endblock %}
{% block user_footer %} {% block user_footer %}
{% if user.first_name %} {% if user.user.first_name %}
<div style="font-weight: 600; display: none" class="fullname gray"> <div style="font-weight: 600; display: none" class="fullname gray">
{{ user.first_name if user.first_name else ''}} {{ user.user.first_name }}
</div> </div>
{% endif %} {% endif %}
{% if user.last_name %} {% if user.user.last_name %}
<div class="school gray" style="display: none"><a style="font-weight: 600"> <div class="school gray" style="display: none"><div style="font-weight: 600">
{{- user.last_name -}} {{- user.user.last_name -}}
</a></div> </div></div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,12 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Courses</title> <title>Courses</title>
</head> </head>
<body> <body>
</body> </body>
</html> </html>

View file

@ -1,19 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title> <title>Document</title>
</head> </head>
<body> <body>
<h1>Enrolling</h1> <h1>Enrolling</h1>
{% for course in enrolling %} {% for course in enrolling %}
<h2> {{ course }} </h2> <h2> {{ course }} </h2>
{% endfor %} {% endfor %}
<h1> Available </h1> <h1> Available </h1>
{% for course in available %} {% for course in available %}
<h2> {{ course }} </h2> <h2> {{ course }} </h2>
{% endfor %} {% endfor %}
</body> </body>
</html> </html>

View file

@ -16,8 +16,9 @@
$('.vote-detail').each(function() { $('.vote-detail').each(function() {
$(this).on('click', function() { $(this).on('click', function() {
var pid = $(this).attr('pid'); var pid = $(this).attr('pid');
$('.detail').hide(); $.get("{{url('internal_problem_votes')}}?id="+pid, function(data) {
$('#detail-'+pid).show(); $('#detail').html(data);
});
}) })
}) })
}); });
@ -59,37 +60,8 @@
{% block right_sidebar %} {% block right_sidebar %}
<div style="display: block; width: 100%"> <div style="display: block; width: 100%">
<div><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('Admin')}}</a></div> <a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('Admin')}}</a>
{% for problem in problems %} <div class="detail" id="detail">
<div class="detail" id="detail-{{problem.id}}" style="display: none;"> </div>
<h3>{{_('Votes for problem') }} {{problem.name}}</h3> </div>
<ol>
{% for vote in problem.volunteer_user_votes.order_by('id') %}
<li>
<h4> {{link_user(vote.voter)}} </h4>
<table class="table">
<tbody>
<tr>
<td style="width:10%">{{_('Knowledge')}}</td>
<td>{{vote.knowledge_points}}</td>
</tr>
<tr>
<td>{{_('Thinking')}}</td>
<td>{{vote.thinking_points}}</td>
</tr>
<tr>
<td>{{_('Types')}}</td>
<td>{{vote.types.all() | join(', ')}}</td>
</tr>
<tr>
<td>{{_('Feedback')}}</td>
<td>{{vote.feedback}}</td>
</tr>
</tbody>
</table>
</li>
{% endfor %}
</ol>
</div>
{% endfor %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,29 @@
<h3>{{_('Votes for problem') }} {{problem.name}}</h3>
<ol>
{% for vote in votes %}
<li>
<h4> {{link_user(vote.voter)}} </h4>
<table class="table">
<tbody>
<tr>
<td style="width:10%">{{_('Knowledge')}}</td>
<td>{{vote.knowledge_points}}</td>
</tr>
<tr>
<td>{{_('Thinking')}}</td>
<td>{{vote.thinking_points}}</td>
</tr>
<tr>
<td>{{_('Types')}}</td>
<td>{{vote.types.all() | join(', ')}}</td>
</tr>
<tr>
<td>{{_('Feedback')}}</td>
<td>{{vote.feedback}}</td>
</tr>
</tbody>
</table>
</li>
{% endfor %}
</ol>
</div>

View file

@ -29,12 +29,6 @@
height: 2em; height: 2em;
padding-top: 4px; padding-top: 4px;
} }
@media(min-width: 800px) {
#content {
width: 99%;
margin-left: 0;
}
}
@media(max-width: 799px) { @media(max-width: 799px) {
#content { #content {
width: 100%; width: 100%;

View file

@ -117,7 +117,6 @@
{% endfor %} {% endfor %}
</span> </span>
{% endif %} {% endif %}
<span class="spacer"></span> <span class="spacer"></span>
{% if has_render %} {% if has_render %}
<a href="{{ url('problem_pdf', problem.code) }}" class="view-pdf" target="_blank"> <a href="{{ url('problem_pdf', problem.code) }}" class="view-pdf" target="_blank">
@ -135,6 +134,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block info_float %} {% block info_float %}
{% if request.user.is_authenticated and request.in_contest_mode and submission_limit %} {% if request.user.is_authenticated and request.in_contest_mode and submission_limit %}
{% if submissions_left > 0 %} {% if submissions_left > 0 %}
@ -204,53 +204,6 @@
</div> </div>
{% endif %} {% endif %}
<hr style="padding-top: 0.3em">
<div class="problem-info-entry">
<i class="fa fa-check fa-fw"></i><span class="pi-name">{{ _('Points:') }}</span>
<span class="pi-value">
{% if contest_problem %}
{{ contest_problem.points }}{% if contest_problem.partial %} {{ _('(partial)') }}{% endif %}
{% else %}
{{ problem.points|floatformat }}{% if problem.partial %} {{ _('(partial)') }}{% endif %}
{% endif %}
</span>
</div>
<div class="problem-info-entry">
<i class="fa fa-clock-o fa-fw"></i><span class="pi-name">{{ _('Time limit:') }}</span>
<span class="pi-value">{{ problem.time_limit }}s</span>
</div>
<div class="problem-lang-limits">
{% for name, limit in problem.language_time_limit %}
<div class="lang-limit">
<span class="lang-name">{{ name }}</span>
<span class="lang-tl">{{ limit }}s</span>
</div>
{% endfor %}
</div>
<div class="problem-info-entry">
<i class="fa fa-server fa-fw"></i><span class="pi-name">{{ _('Memory limit:') }}</span>
<span class="pi-value">{{ problem.memory_limit|kbsimpleformat }}</span>
</div>
<div class="problem-lang-limits">
{% for name, limit in problem.language_memory_limit %}
<div class="lang-limit">
<span class="lang-name">{{ name }}</span>
<span class="lang-ml">{{ limit|kbsimpleformat }}</span>
</div>
{% endfor %}
</div>
<div class="problem-info-entry">
<i class="fa fa-file-o fa-fw"></i><span class="pi-name">{{ _('Input:') }}</span>
<span class="pi-value">
{{ fileio_input or _('stdin') }}
</span>
</div>
<div class="problem-info-entry">
<i class="fa fa-file fa-fw"></i><span class="pi-name">{{ _('Output:') }}</span>
<span class="pi-value">{{ fileio_output or _('stdout') }}</span>
</div>
<hr style="padding-top: 0.7em"> <hr style="padding-top: 0.7em">
{% cache 86400 'problem_authors' problem.id LANGUAGE_CODE %} {% cache 86400 'problem_authors' problem.id LANGUAGE_CODE %}
@ -342,6 +295,40 @@
</a> </a>
</div> </div>
{% endif %} {% endif %}
<div class = "new-problem-info center">
<span class="info-block">
<i class="fa fa-check fa-fw"></i><span class="pi-name">{{ _('Points:') }}</span>
<span class="new-pi-value">
{% if contest_problem %}
{{ contest_problem.points }} {% if contest_problem.partial %}(p){% endif %}
{% else %}
{{ problem.points|floatformat }} {% if problem.partial %}(p){% endif %}
{% endif %}
</span>
</span>
<span class="info-block">
<i class="fa fa-clock-o fa-fw"></i><span class="pi-name">{{ _('Time limit:') }}</span>
<span class="new-pi-value">{{ problem.time_limit }}s</span>
</span>
<span class="info-block">
<i class="fa fa-server fa-fw"></i><span class="pi-name">{{ _('Memory limit:') }}</span>
<span class="new-pi-value">{{ problem.memory_limit|kbsimpleformat }}</span>
</span>
<span class="info-block">
<i class="fa fa-file-o fa-fw"></i><span class="pi-name">{{ _('Input:') }}</span>
<span class="new-pi-value">
{{ fileio_input or _('stdin') }}
</span>
</span>
<span class="info-block">
<i class="fa fa-file fa-fw"></i><span class="pi-name">{{ _('Output:') }}</span>
<span class="new-pi-value">{{ fileio_output or _('stdout') }}</span>
</span>
</div>
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %} {% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
{{ description|markdown(lazy_load=True)|reference|str|safe }} {{ description|markdown(lazy_load=True)|reference|str|safe }}

View file

@ -28,6 +28,8 @@
{% block js_media %} {% block js_media %}
<script type="text/javascript"> <script type="text/javascript">
let loadingPage;
function activateBlogBoxOnClick() { function activateBlogBoxOnClick() {
$('.blog-box').on('click', function () { $('.blog-box').on('click', function () {
var $description = $(this).children('.blog-description'); var $description = $(this).children('.blog-description');
@ -64,13 +66,16 @@
$elem.addClass('active'); $elem.addClass('active');
} }
$(window).off("scroll"); $(window).off("scroll");
$('.middle-right-content').html(loading_page); $('.middle-right-content').html(loadingPage);
$.get(url, function (data) { $.get(url, function (data) {
var reload_content = $(data).find('.middle-right-content'); var reload_content = $(data).find('.middle-right-content');
var bodyend_script = $(data).find('#extra_js');
if (reload_content.length) { if (reload_content.length) {
window.history.pushState("", "", url); window.history.pushState("", "", url);
$('html, body').animate({scrollTop: 0}, 'fast'); $('html, body').animate({scrollTop: 0}, 'fast');
$('.middle-right-content').html(reload_content.first().html()); $('.middle-right-content').html(reload_content.first().html());
$('#extra_js').html(bodyend_script.first().html());
$("#loading-bar").hide().css({ width: 0});
if (reload_content.hasClass("wrapper")) { if (reload_content.hasClass("wrapper")) {
$('.middle-right-content').addClass("wrapper"); $('.middle-right-content').addClass("wrapper");
} }
@ -83,7 +88,6 @@
activateBlogBoxOnClick(); activateBlogBoxOnClick();
$('.xdsoft_datetimepicker').hide(); $('.xdsoft_datetimepicker').hide();
registerNavigation(); registerNavigation();
$("#loading-bar").hide().css({ width: 0});
} }
else { else {
window.location.href = url; window.location.href = url;
@ -112,6 +116,9 @@
navigateTo($(this), true); navigateTo($(this), true);
}); });
registerNavigation(); registerNavigation();
$.get("{{static('html/loading-page.html')}}", function(data) {
loadingPage = data;
});
}); });
</script> </script>
{% endblock %} {% endblock %}
@ -129,7 +136,6 @@
{% block left_sidebar %}{% endblock %} {% block left_sidebar %}{% endblock %}
<div class="middle-right-content {{'wrapper' if is_two_column}}"> <div class="middle-right-content {{'wrapper' if is_two_column}}">
{% block three_col_media %}{% endblock %} {% block three_col_media %}{% endblock %}
{% block three_col_js %}{% endblock %}
<div class="middle-content"> <div class="middle-content">
{% block middle_title %}{% endblock %} {% block middle_title %}{% endblock %}
{% block middle_content %}{% endblock %} {% block middle_content %}{% endblock %}
@ -140,6 +146,10 @@
{% block after_posts %}{% endblock %} {% block after_posts %}{% endblock %}
{% endblock %} {% endblock %}
{% block extra_js %}
{% block three_col_js %}{% endblock %}
{% endblock %}
{% block bodyend %} {% block bodyend %}
{{ super() }} {{ super() }}
{% if REQUIRE_JAX %} {% if REQUIRE_JAX %}