Merge pull request #7 from LQDJudge/master

Update 16 Nov
This commit is contained in:
Van Duc Le 2023-11-16 12:23:14 -06:00 committed by GitHub
commit 9211bd1788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 3223 additions and 1794 deletions

View file

@ -11,3 +11,7 @@ repos:
rev: 22.12.0
hooks:
- 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.models import CASCADE, Q
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile
@ -17,22 +18,40 @@ class Room(models.Model):
user_two = models.ForeignKey(
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:
app_label = "chat_box"
@cache_wrapper(prefix="Rinfo")
def _info(self):
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()
@cache_wrapper(prefix="Rc")
def contain(self, profile):
return self.user_one == profile or self.user_two == profile
return profile.id in self._cached_info["user_ids"]
@cache_wrapper(prefix="Rou")
def other_user(self, profile):
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):
return [self.user_one, self.user_two]
@cache_wrapper(prefix="Rlmb")
def last_message_body(self):
return self.message_set.first().body
return self._cached_info["last_message"]
class Message(models.Model):
@ -58,6 +77,7 @@ class Message(models.Model):
indexes = [
models.Index(fields=["hidden", "room", "-id"]),
]
app_label = "chat_box"
class UserRoom(models.Model):
@ -70,6 +90,7 @@ class UserRoom(models.Model):
class Meta:
unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model):
@ -82,6 +103,9 @@ class Ignore(models.Model):
)
ignored_users = models.ManyToManyField(Profile)
class Meta:
app_label = "chat_box"
@classmethod
def is_ignored(self, current_user, new_friend):
try:

View file

@ -8,6 +8,8 @@ from django.db.models.functions import Coalesce
from chat_box.models import Ignore, Message, UserRoom, Room
from judge.caching import cache_wrapper
secret_key = settings.CHAT_SECRET_KEY
fernet = Fernet(secret_key)
@ -37,6 +39,7 @@ def encrypt_channel(channel):
)
@cache_wrapper(prefix="gub")
def get_unread_boxes(profile):
ignored_rooms = Ignore.get_ignored_rooms(profile)
unread_boxes = (

View file

@ -29,16 +29,13 @@ from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django.urls import reverse
import datetime
from judge import event_poster as event
from judge.jinja2.gravatar import gravatar
from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel
import json
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
class ChatView(ListView):
@ -87,8 +84,8 @@ class ChatView(ListView):
self.room_id = request_room
self.messages = (
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
.select_related("author", "author__user")
.defer("author__about", "author__user_script")[:page_size]
.select_related("author")
.only("body", "time", "author__rating", "author__display_rank")[:page_size]
)
if not only_messages:
return super().get(request, *args, **kwargs)
@ -207,7 +204,10 @@ def post_message(request):
},
)
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():
event.post(
encrypt_channel("chat_" + str(user.id)),
@ -223,6 +223,7 @@ def post_message(request):
UserRoom.objects.filter(user=user, room=room).update(
unread_count=F("unread_count") + 1
)
get_unread_boxes.dirty(user)
return JsonResponse(ret)
@ -285,6 +286,8 @@ def update_last_seen(request, **kwargs):
user_room.unread_count = 0
user_room.save()
get_unread_boxes.dirty(profile)
return JsonResponse({"msg": "updated"})
@ -350,11 +353,11 @@ def get_online_status(profile, other_profile_ids, rooms=None):
room = Room.objects.get(id=i["room"])
other_profile = room.other_user(profile)
count[other_profile.id] = i["unread_count"]
rooms = Room.objects.filter(id__in=rooms)
for room in rooms:
room = Room.objects.get(id=room)
other_profile = room.other_user(profile)
last_msg[other_profile.id] = room.last_message_body()
room_of_user[other_profile.id] = room.id
other_profile_id = room.other_user_id(profile)
last_msg[other_profile_id] = room.last_message_body()
room_of_user[other_profile_id] = room.id
for other_profile in other_profiles:
is_online = False
@ -388,9 +391,6 @@ def get_status_context(profile, include_ignored=False):
recent_profile = (
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
.annotate(
last_msg_time=Subquery(
Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
),
other_user=Case(
When(user_one=profile, then="user_two"),
default="user_one",
@ -411,28 +411,15 @@ def get_status_context(profile, include_ignored=False):
.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 [
{
"title": "Recent",
"title": _("Recent"),
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
},
{
"title": "Admin",
"title": _("Admin"),
"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, ...)
import datetime
import os
import tempfile
@ -485,6 +484,9 @@ META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR"
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
try:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
exec(f.read(), globals())

View file

@ -44,6 +44,7 @@ from judge.views import (
language,
license,
mailgun,
markdown_editor,
notification,
organization,
preview,
@ -405,6 +406,11 @@ urlpatterns = [
]
),
),
url(
r"^markdown_editor/",
markdown_editor.MarkdownEditor.as_view(),
name="markdown_editor",
),
url(
r"^submission_source_file/(?P<filename>(\w|\.)+)",
submission.SubmissionSourceFileView.as_view(),
@ -511,6 +517,11 @@ urlpatterns = [
),
),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
url(
r"^contests/summary/(?P<key>\w+)$",
contests.contests_summary_view,
name="contests_summary",
),
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
url(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
@ -1089,6 +1100,11 @@ urlpatterns = [
internal.InternalProblem.as_view(),
name="internal_problem",
),
url(
r"^problem_votes$",
internal.get_problem_votes,
name="internal_problem_votes",
),
url(
r"^request_time$",
internal.InternalRequestTime.as_view(),

View file

@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
from judge.admin.contest import (
ContestAdmin,
ContestParticipationAdmin,
ContestTagAdmin,
ContestsSummaryAdmin,
)
from judge.admin.interface import (
BlogPostAdmin,
LicenseAdmin,
@ -41,6 +46,7 @@ from judge.models import (
Ticket,
VolunteerProblemVote,
Course,
ContestsSummary,
)
@ -69,3 +75,4 @@ admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.register(ContestsSummary, ContestsSummaryAdmin)

View file

@ -502,3 +502,19 @@ class ContestParticipationAdmin(admin.ModelAdmin):
show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "virtual"
class ContestsSummaryForm(ModelForm):
class Meta:
widgets = {
"contests": AdminHeavySelect2MultipleWidget(
data_view="contest_select2", attrs={"style": "width: 100%"}
),
}
class ContestsSummaryAdmin(admin.ModelAdmin):
fields = ("key", "contests", "scores")
list_display = ("key",)
search_fields = ("key", "contests__key")
form = ContestsSummaryForm

View file

@ -25,6 +25,7 @@ from judge.models import (
Solution,
Notification,
)
from judge.models.notification import make_notification
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget,
@ -32,6 +33,7 @@ from judge.widgets import (
CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget,
)
from judge.utils.problems import user_editable_ids, user_tester_ids
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
@ -358,12 +360,31 @@ class ProblemAdmin(CompareVersionAdmin):
self._rescore(request, obj.id)
def save_related(self, request, form, formsets, change):
editors = set()
testers = set()
if "curators" in form.changed_data or "authors" in form.changed_data:
editors = set(form.instance.editor_ids)
if "testers" in form.changed_data:
testers = set(form.instance.tester_ids)
super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model`
obj = form.instance
obj.curators.add(request.profile)
obj.is_organization_private = obj.organizations.count() > 0
obj.save()
if "curators" in form.changed_data or "authors" in form.changed_data:
del obj.editor_ids
editors = editors.union(set(obj.editor_ids))
if "testers" in form.changed_data:
del obj.tester_ids
testers = testers.union(set(obj.tester_ids))
for editor in editors:
user_editable_ids.dirty(editor)
for tester in testers:
user_tester_ids.dirty(tester)
# Create notification
if "is_public" in form.changed_data or "organizations" in form.changed_data:
users = set(obj.authors.all())
@ -381,14 +402,7 @@ class ProblemAdmin(CompareVersionAdmin):
category = "Problem public: " + str(obj.is_public)
if orgs:
category += " (" + ", ".join(orgs) + ")"
for user in users:
notification = Notification(
owner=user,
html_link=html,
category=category,
author=request.profile,
)
notification.save()
make_notification(users, category, html, request.profile)
def construct_change_message(self, request, form, *args, **kwargs):
if form.cleaned_data.get("change_message"):

View file

@ -126,7 +126,7 @@ class ProfileAdmin(VersionAdmin):
admin_user_admin.short_description = _("User")
def email(self, obj):
return obj.user.email
return obj.email
email.admin_order_field = "user__email"
email.short_description = _("Email")

View file

@ -3,7 +3,6 @@ import json
import logging
import threading
import time
import os
from collections import deque, namedtuple
from operator import itemgetter
@ -25,6 +24,7 @@ from judge.models import (
Submission,
SubmissionTestCase,
)
from judge.bridge.utils import VanishedSubmission
logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge")
@ -94,12 +94,6 @@ class JudgeHandler(ZlibPacketHandler):
def on_disconnect(self):
self._stop_ping.set()
if 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()
@ -119,16 +113,6 @@ class JudgeHandler(ZlibPacketHandler):
None,
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):
try:
@ -327,6 +311,9 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source):
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_data = {
"problem": problem,
@ -675,8 +662,11 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet)
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(
status="IE", result="IE", error=packet["message"]
status="IE", result="IE", error=message
):
event.post(
"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)
json_log.info(
self._make_json_log(
packet,
sub=id,
action="internal-error",
message=packet["message"],
message=message,
finish=True,
result="IE",
)
@ -695,10 +685,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id)
json_log.error(
self._make_json_log(
packet,
sub=id,
action="internal-error",
info="unknown submission",
message=packet["message"],
message=message,
finish=True,
result="IE",
)

View file

@ -3,6 +3,8 @@ from collections import namedtuple
from operator import attrgetter
from threading import RLock
from judge.bridge.utils import VanishedSubmission
try:
from llist import dllist
except ImportError:
@ -39,6 +41,8 @@ class JudgeList(object):
)
try:
judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception:
logger.exception(
"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

@ -1,49 +1,78 @@
from inspect import signature
from django.core.cache import cache
from django.core.cache import cache, caches
from django.db.models.query import QuerySet
from django.core.handlers.wsgi import WSGIRequest
import hashlib
MAX_NUM_CHAR = 15
MAX_NUM_CHAR = 50
NONE_RESULT = "__None__"
def cache_wrapper(prefix, timeout=None):
def arg_to_str(arg):
if hasattr(arg, "id"):
return str(arg.id)
if isinstance(arg, list) or isinstance(arg, QuerySet):
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
if len(str(arg)) > MAX_NUM_CHAR:
return str(arg)[:MAX_NUM_CHAR]
return str(arg)
def arg_to_str(arg):
if hasattr(arg, "id"):
return str(arg.id)
if isinstance(arg, list) or isinstance(arg, QuerySet):
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
if len(str(arg)) > MAX_NUM_CHAR:
return str(arg)[:MAX_NUM_CHAR]
return str(arg)
def filter_args(args_list):
return [x for x in args_list if not isinstance(x, WSGIRequest)]
l0_cache = caches["l0"] if "l0" in caches else None
def cache_wrapper(prefix, timeout=None):
def get_key(func, *args, **kwargs):
args_list = list(args)
signature_args = list(signature(func).parameters.keys())
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
args_list = filter_args(args_list)
args_list = [arg_to_str(i) for i in args_list]
key = prefix + ":" + ":".join(args_list)
key = key.replace(" ", "_")
return key
def _get(key):
if not l0_cache:
return cache.get(key)
result = l0_cache.get(key)
if result is None:
result = cache.get(key)
return result
def _set_l0(key, value):
if l0_cache:
l0_cache.set(key, value, 30)
def _set(key, value, timeout):
_set_l0(key, value)
cache.set(key, value, timeout)
def decorator(func):
def wrapper(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
result = cache.get(cache_key)
result = _get(cache_key)
if result is not None:
if result == NONE_RESULT:
_set_l0(cache_key, result)
if type(result) == str and result == NONE_RESULT:
result = None
return result
result = func(*args, **kwargs)
if result is None:
result = NONE_RESULT
result = func(*args, **kwargs)
cache.set(cache_key, result, timeout)
_set(cache_key, result, timeout)
return result
def dirty(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
cache.delete(cache_key)
if l0_cache:
l0_cache.delete(cache_key)
wrapper.dirty = dirty

View file

@ -26,21 +26,20 @@ from judge.dblock import LockModel
from judge.models import Comment, Notification
from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text
from judge.models.notification import make_notification
DEFAULT_OFFSET = 10
def _get_html_link_notification(comment):
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
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.save()
def del_mention_notifications(comment):
query = {"comment": comment, "category": "Mention"}
Notification.objects.filter(**query).delete()
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
link = _get_html_link_notification(comment)
make_notification(users_mentioned, "Mention", link, comment.author)
class CommentForm(ModelForm):
@ -124,23 +123,17 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment.save()
# add notification for reply
comment_notif_link = _get_html_link_notification(comment)
if comment.parent and comment.parent.author != comment.author:
notification_reply = Notification(
owner=comment.parent.author, comment=comment, category="Reply"
make_notification(
[comment.parent.author], "Reply", comment_notif_link, comment.author
)
notification_reply.save()
# add notification for page authors
page_authors = comment.linked_object.authors.all()
for user in page_authors:
if user == comment.author:
continue
notification = Notification(
owner=user, comment=comment, category="Comment"
)
notification.save()
# except Exception:
# pass
make_notification(
page_authors, "Comment", comment_notif_link, comment.author
)
add_mention_notifications(comment)
@ -151,14 +144,16 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
target_comment = None
self.object = self.get_object()
if "comment-id" in request.GET:
comment_id = int(request.GET["comment-id"])
try:
comment_id = int(request.GET["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
target_comment = comment_obj.get_root()
self.object = self.get_object()
return self.render_to_response(
self.get_context_data(
object=self.object,
@ -168,7 +163,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
)
def _get_queryset(self, target_comment):
if target_comment != None:
if target_comment:
queryset = target_comment.get_descendants(include_self=True)
queryset = (
queryset.select_related("author__user")
@ -217,11 +212,11 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
context["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked()
context["comment_list"] = queryset
context["comment_list"] = list(queryset)
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
if queryset.exists():
context["comment_root_id"] = queryset[0].id
context["comment_root_id"] = context["comment_list"][0].id
else:
context["comment_root_id"] = 0
context["comment_parent_none"] = 1
@ -234,4 +229,5 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
context["limit"] = DEFAULT_OFFSET
context["comment_count"] = comment_count
context["profile"] = self.request.profile
return context

View file

@ -11,7 +11,6 @@ from django.contrib.auth.models import User
from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.db import transaction
from django.db.models import Q
from django.forms import (
CharField,
@ -52,7 +51,6 @@ from judge.widgets import (
DateTimePickerWidget,
ImageWidget,
)
from judge.tasks import rescore_contest
def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
@ -282,16 +280,9 @@ class EditOrganizationContestForm(ModelForm):
"view_contest_scoreboard",
]:
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:
model = Contest
fields = (

View file

@ -21,7 +21,6 @@ from . import (
render,
social,
spaceless,
submission,
timedelta,
)
from . import registry

View file

@ -12,12 +12,12 @@ from . import registry
def gravatar(profile, size=80, default=None, profile_image=None, email=None):
if profile_image:
return profile_image
if profile and profile.profile_image:
return profile.profile_image.url
if profile and profile.profile_image_url:
return profile.profile_image_url
if profile:
email = email or profile.user.email
email = email or profile.email
if default is None:
default = profile.mute
default = profile.is_muted
gravatar_url = (
"//www.gravatar.com/avatar/"
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()

View file

@ -157,14 +157,14 @@ def item_title(item):
@registry.render_with("user/link.html")
def link_user(user):
if isinstance(user, Profile):
user, profile = user.user, user
profile = user
elif isinstance(user, AbstractUser):
profile = user.profile
elif type(user).__name__ == "ContestRankingProfile":
user, profile = user.user, user
profile = user
else:
raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"user": user, "profile": profile}
return {"profile": profile}
@registry.function

View file

@ -1,41 +0,0 @@
from . import registry
@registry.function
def submission_layout(
submission,
profile_id,
user,
editable_problem_ids,
completed_problem_ids,
tester_problem_ids,
):
problem_id = submission.problem_id
if problem_id in editable_problem_ids:
return True
if problem_id in tester_problem_ids:
return True
if profile_id == submission.user_id:
return True
if user.has_perm("judge.change_submission"):
return True
if user.has_perm("judge.view_all_submission"):
return True
if submission.problem.is_public and user.has_perm("judge.view_public_submission"):
return True
if hasattr(submission, "contest"):
contest = submission.contest.participation.contest
if contest.is_editable_by(user):
return True
if submission.problem_id in completed_problem_ids and submission.problem.is_public:
return True
return False

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 judge.models import *
from collections import defaultdict
import csv
import os
from django.conf import settings

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,30 @@
# Generated by Django 3.2.21 on 2023-10-02 03:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0169_public_scoreboard"),
]
operations = [
migrations.CreateModel(
name="ContestsSummary",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("scores", models.JSONField(blank=True, null=True)),
("key", models.CharField(max_length=20, unique=True)),
("contests", models.ManyToManyField(to="judge.Contest")),
],
),
]

View file

@ -0,0 +1,68 @@
# Generated by Django 3.2.18 on 2023-10-10 21:17
from django.db import migrations, models
import django.db.models.deletion
from django.urls import reverse
from collections import defaultdict
# Run this in shell
def migrate_notif(apps, schema_editor):
Notification = apps.get_model("judge", "Notification")
Profile = apps.get_model("judge", "Profile")
NotificationProfile = apps.get_model("judge", "NotificationProfile")
unread_count = defaultdict(int)
for c in Notification.objects.all():
if c.comment:
c.html_link = (
f'<a href="{c.comment.get_absolute_url()}">{c.comment.page_title}</a>'
)
c.author = c.comment.author
c.save()
if c.read is False:
unread_count[c.author] += 1
for user in unread_count:
np = NotificationProfile(user=user)
np.unread_count = unread_count[user]
np.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0170_contests_summary"),
]
operations = [
migrations.AlterModelOptions(
name="contestssummary",
options={
"verbose_name": "contests summary",
"verbose_name_plural": "contests summaries",
},
),
migrations.CreateModel(
name="NotificationProfile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("unread_count", models.IntegerField(default=0)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to="judge.profile"
),
),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-10-10 23:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0171_update_notification"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="rating",
field=models.IntegerField(db_index=True, default=None, null=True),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.18 on 2023-10-14 00:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("judge", "0172_index_rating"),
]
operations = [
migrations.RunSQL(
(
"CREATE FULLTEXT INDEX IF NOT EXISTS code_name_index ON judge_problem (code, name)",
),
reverse_sql=migrations.RunSQL.noop,
),
migrations.RunSQL(
(
"CREATE FULLTEXT INDEX IF NOT EXISTS key_name_index ON judge_contest (`key`, name)",
),
reverse_sql=migrations.RunSQL.noop,
),
]

View file

@ -1,9 +1,10 @@
import numpy as np
from django.conf import settings
import os
from django.core.cache import cache
import hashlib
from django.core.cache import cache
from django.conf import settings
from judge.caching import cache_wrapper
@ -13,14 +14,13 @@ class CollabFilter:
# name = 'collab_filter' or 'collab_filter_time'
def __init__(self, name):
embeddings = np.load(
self.embeddings = np.load(
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
allow_pickle=True,
)
arr0, arr1 = embeddings.files
_, problem_arr = self.embeddings.files
self.name = name
self.user_embeddings = embeddings[arr0]
self.problem_embeddings = embeddings[arr1]
self.problem_embeddings = self.embeddings[problem_arr]
def __str__(self):
return self.name
@ -44,18 +44,32 @@ class CollabFilter:
scores = u.dot(V.T)
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)
def user_recommendations(self, user, problems, measure=DOT, limit=None):
uid = user.id
if uid >= len(self.user_embeddings):
uid = 0
scores = self.compute_scores(
self.user_embeddings[uid], self.problem_embeddings, measure
)
def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
user_embedding = self.get_user_embedding(user_id)
scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
res = [] # [(score, problem)]
for pid in problems:
# pid = problem.id
if pid < len(scores):
res.append((scores[pid], pid))

View file

@ -6,7 +6,7 @@ from judge.models.choices import (
MATH_ENGINES_CHOICES,
TIMEZONE,
)
from judge.models.comment import Comment, CommentLock, CommentVote, Notification
from judge.models.comment import Comment, CommentLock, CommentVote
from judge.models.contest import (
Contest,
ContestMoss,
@ -16,6 +16,7 @@ from judge.models.contest import (
ContestTag,
Rating,
ContestProblemClarification,
ContestsSummary,
)
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread
@ -57,6 +58,7 @@ from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark
from judge.models.course import Course
from judge.models.notification import Notification, NotificationProfile
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"])

View file

@ -177,29 +177,3 @@ class CommentLock(models.Model):
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=1000)
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,
)

View file

@ -24,6 +24,7 @@ from judge.models.submission import Submission
from judge.ratings import rate_contest
from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable
from judge.fulltext import SearchManager
__all__ = [
"Contest",
@ -33,6 +34,7 @@ __all__ = [
"ContestSubmission",
"Rating",
"ContestProblemClarification",
"ContestsSummary",
]
@ -97,11 +99,13 @@ class Contest(models.Model, PageVotable, Bookmarkable):
)
authors = models.ManyToManyField(
Profile,
verbose_name=_("authors"),
help_text=_("These users will be able to edit the contest."),
related_name="authors+",
)
curators = models.ManyToManyField(
Profile,
verbose_name=_("curators"),
help_text=_(
"These users will be able to edit the contest, "
"but will not be listed as authors."
@ -111,6 +115,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
)
testers = models.ManyToManyField(
Profile,
verbose_name=_("testers"),
help_text=_(
"These users will be able to view the contest, " "but not edit it."
),
@ -308,6 +313,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
comments = GenericRelation("Comment")
pagevote = GenericRelation("PageVote")
bookmark = GenericRelation("BookMark")
objects = SearchManager(("key", "name"))
@cached_property
def format_class(self):
@ -900,3 +906,27 @@ class ContestProblemClarification(models.Model):
date = models.DateTimeField(
verbose_name=_("clarification timestamp"), auto_now_add=True
)
class ContestsSummary(models.Model):
contests = models.ManyToManyField(
Contest,
)
scores = models.JSONField(
null=True,
blank=True,
)
key = models.CharField(
max_length=20,
unique=True,
)
class Meta:
verbose_name = _("contests summary")
verbose_name_plural = _("contests summaries")
def __str__(self):
return self.key
def get_absolute_url(self):
return reverse("contests_summary", args=[self.key])

View file

@ -0,0 +1,61 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.db.models import CASCADE, F
from django.core.exceptions import ObjectDoesNotExist
from judge.models import Profile, Comment
from judge.caching import cache_wrapper
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)
category = models.CharField(verbose_name=_("category"), max_length=1000)
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,
)
comment = models.ForeignKey(
Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE
) # deprecated
read = models.BooleanField(verbose_name=_("read"), default=False) # deprecated
class NotificationProfile(models.Model):
unread_count = models.IntegerField(default=0)
user = models.OneToOneField(Profile, on_delete=CASCADE)
def make_notification(to_users, category, html_link, author):
for user in to_users:
if user == author:
continue
notif = Notification(
owner=user, category=category, html_link=html_link, author=author
)
notif.save()
NotificationProfile.objects.get_or_create(user=user)
NotificationProfile.objects.filter(user=user).update(
unread_count=F("unread_count") + 1
)
unseen_notifications_count.dirty(user)
@cache_wrapper(prefix="unc")
def unseen_notifications_count(profile):
try:
return NotificationProfile.objects.get(user=profile).unread_count
except ObjectDoesNotExist:
return 0

View file

@ -5,6 +5,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["PageVote", "PageVoteVoter"]
@ -28,6 +29,7 @@ class PageVote(models.Model):
]
unique_together = ("content_type", "object_id")
@cache_wrapper(prefix="PVvs")
def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
if page_vote.exists():

View file

@ -1,6 +1,5 @@
import errno
from operator import attrgetter
from math import sqrt
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
@ -107,9 +106,7 @@ class License(models.Model):
class TranslatedProblemQuerySet(SearchQuerySet):
def __init__(self, **kwargs):
super(TranslatedProblemQuerySet, self).__init__(
("code", "name", "description"), **kwargs
)
super(TranslatedProblemQuerySet, self).__init__(("code", "name"), **kwargs)
def add_i18n_name(self, language):
return self.annotate(
@ -436,15 +433,23 @@ class Problem(models.Model, PageVotable, Bookmarkable):
@cached_property
def author_ids(self):
return self.authors.values_list("id", flat=True)
return Problem.authors.through.objects.filter(problem=self).values_list(
"profile_id", flat=True
)
@cached_property
def editor_ids(self):
return self.author_ids | self.curators.values_list("id", flat=True)
return self.author_ids.union(
Problem.curators.through.objects.filter(problem=self).values_list(
"profile_id", flat=True
)
)
@cached_property
def tester_ids(self):
return self.testers.values_list("id", flat=True)
return Problem.testers.through.objects.filter(problem=self).values_list(
"profile_id", flat=True
)
@cached_property
def usable_common_names(self):
@ -551,7 +556,7 @@ class Problem(models.Model, PageVotable, Bookmarkable):
def save(self, *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:
try:
problem_data_storage.rename(self.__original_code, self.code)

View file

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

View file

@ -10,12 +10,17 @@ from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
from fernet_fields import EncryptedCharField
from sortedm2m.fields import SortedManyToManyField
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
from judge.models.runtime import Language
from judge.ratings import rating_class
from judge.caching import cache_wrapper
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
@ -142,6 +147,7 @@ class Organization(models.Model):
)
verbose_name = _("organization")
verbose_name_plural = _("organizations")
app_label = "judge"
class Profile(models.Model):
@ -200,7 +206,7 @@ class Profile(models.Model):
help_text=_("User will not be able to vote on problems' point values."),
default=False,
)
rating = models.IntegerField(null=True, default=None)
rating = models.IntegerField(null=True, default=None, db_index=True)
user_script = models.TextField(
verbose_name=_("user script"),
default="",
@ -254,6 +260,24 @@ class Profile(models.Model):
max_length=300,
)
@cache_wrapper(prefix="Pgbi2")
def _get_basic_info(self):
profile_image_url = None
if self.profile_image:
profile_image_url = self.profile_image.url
return {
"first_name": self.user.first_name,
"last_name": self.user.last_name,
"email": self.user.email,
"username": self.user.username,
"mute": self.mute,
"profile_image_url": profile_image_url,
}
@cached_property
def _cached_info(self):
return self._get_basic_info()
@cached_property
def organization(self):
# We do this to take advantage of prefetch_related
@ -262,14 +286,33 @@ class Profile(models.Model):
@cached_property
def username(self):
return self.user.username
return self._cached_info["username"]
@cached_property
def first_name(self):
return self._cached_info["first_name"]
@cached_property
def last_name(self):
return self._cached_info["last_name"]
@cached_property
def email(self):
return self._cached_info["email"]
@cached_property
def is_muted(self):
return self._cached_info["mute"]
@cached_property
def profile_image_url(self):
return self._cached_info["profile_image_url"]
@cached_property
def count_unseen_notifications(self):
query = {
"read": False,
}
return self.notifications.filter(**query).count()
from judge.models.notification import unseen_notifications_count
return unseen_notifications_count(self)
@cached_property
def count_unread_chat_boxes(self):
@ -498,3 +541,21 @@ class OrganizationProfile(models.Model):
@classmethod
def get_most_recent_organizations(self, users):
return self.objects.filter(users=users).order_by("-last_visit")[:5]
@receiver([post_save], sender=User)
def on_user_save(sender, instance, **kwargs):
try:
profile = instance.profile
profile._get_basic_info.dirty(profile)
except:
pass
@receiver([pre_save], sender=Profile)
def on_profile_save(sender, instance, **kwargs):
if instance.id is None:
return
prev = sender.objects.get(id=instance.id)
if prev.mute != instance.mute or prev.profile_image != instance.profile_image:
instance._get_basic_info.dirty(instance)

View file

@ -220,6 +220,46 @@ class Submission(models.Model):
def id_secret(self):
return self.get_id_secret(self.id)
def is_accessible_by(self, profile):
from judge.utils.problems import (
user_completed_ids,
user_tester_ids,
user_editable_ids,
)
if not profile:
return False
problem_id = self.problem_id
user = profile.user
if profile.id == self.user_id:
return True
if problem_id in user_editable_ids(profile):
return True
if self.problem_id in user_completed_ids(profile):
if self.problem.is_public:
return True
if problem_id in user_tester_ids(profile):
return True
if user.has_perm("judge.change_submission"):
return True
if user.has_perm("judge.view_all_submission"):
return True
if self.problem.is_public and user.has_perm("judge.view_public_submission"):
return True
contest = self.contest_object
if contest and contest.is_editable_by(user):
return True
return False
class Meta:
permissions = (
("abort_any_submission", "Abort any submission"),

View file

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

View file

@ -5,7 +5,6 @@ from django import forms
from django.forms import ClearableFileInput
import os, os.path
import tempfile
import shutil
__all__ = ("handle_upload", "save_upload", "FineUploadForm", "FineUploadFileInput")
@ -35,7 +34,7 @@ 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 = settings.CHUNK_UPLOAD_DIR
if not os.path.exists(os.path.dirname(chunks_dir)):
os.makedirs(os.path.dirname(chunks_dir))
chunked = False

View file

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

View file

@ -1,8 +1,8 @@
from collections import defaultdict
from math import e
import os, zipfile
from datetime import datetime
from datetime import datetime, timedelta
import random
from enum import Enum
from django.conf import settings
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.utils import timezone
from django.utils.translation import gettext as _, gettext_noop
from django.http import Http404
from judge.models import Problem, Submission
from judge.ml.collab_filter import CollabFilter
@ -24,40 +25,41 @@ __all__ = [
]
@cache_wrapper(prefix="user_tester")
def user_tester_ids(profile):
return set(
Problem.testers.through.objects.filter(profile=profile).values_list(
"problem_id", flat=True
)
Problem.testers.through.objects.filter(profile=profile)
.values_list("problem_id", flat=True)
.distinct()
)
@cache_wrapper(prefix="user_editable")
def user_editable_ids(profile):
result = set(
(
Problem.objects.filter(authors=profile)
| Problem.objects.filter(curators=profile)
).values_list("id", flat=True)
)
.values_list("id", flat=True)
.distinct()
)
return result
@cache_wrapper(prefix="contest_complete")
def contest_completed_ids(participation):
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")
)
cache.set(key, result, 86400)
.values_list("problem__problem__id", flat=True)
.distinct()
)
return result
@cache_wrapper(prefix="user_complete", timeout=86400)
@cache_wrapper(prefix="user_complete")
def user_completed_ids(profile):
result = set(
Submission.objects.filter(
@ -69,7 +71,7 @@ def user_completed_ids(profile):
return result
@cache_wrapper(prefix="contest_attempted", timeout=86400)
@cache_wrapper(prefix="contest_attempted")
def contest_attempted_ids(participation):
result = {
id: {"achieved_points": points, "max_points": max_points}
@ -84,7 +86,7 @@ def contest_attempted_ids(participation):
return result
@cache_wrapper(prefix="user_attempted", timeout=86400)
@cache_wrapper(prefix="user_attempted")
def user_attempted_ids(profile):
result = {
id: {
@ -248,3 +250,72 @@ def finished_submission(sub):
keys += ["contest_complete:%d" % participation.id]
keys += ["contest_attempted:%d" % participation.id]
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

@ -15,12 +15,11 @@ from django.http import (
HttpResponseBadRequest,
HttpResponseForbidden,
)
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView
from django.urls import reverse_lazy
from django.template import loader
from reversion import revisions
from reversion.models import Version
@ -28,7 +27,7 @@ from judge.dblock import LockModel
from judge.models import Comment, CommentVote, Notification, BlogPost
from judge.utils.views import TitleMixin
from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget
from judge.comments import add_mention_notifications, del_mention_notifications
from judge.comments import add_mention_notifications
import json
@ -42,11 +41,6 @@ __all__ = [
@login_required
# def get_more_reply(request, id):
# queryset = Comment.get_pk(id)
def vote_comment(request, delta):
if abs(delta) != 1:
return HttpResponseBadRequest(
@ -156,6 +150,7 @@ def get_comments(request, limit=10):
revisions=Count("versions", distinct=True),
)[offset : offset + limit]
)
profile = None
if request.user.is_authenticated:
profile = request.profile
queryset = queryset.annotate(
@ -164,10 +159,11 @@ def get_comments(request, limit=10):
new_offset = offset + min(len(queryset), limit)
comment_html = loader.render_to_string(
return render(
request,
"comments/content-list.html",
{
"request": request,
"profile": profile,
"comment_root_id": comment_root_id,
"comment_list": queryset,
"vote_hide_threshold": settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD,
@ -181,8 +177,6 @@ def get_comments(request, limit=10):
},
)
return HttpResponse(comment_html)
def get_show_more(request):
return get_comments(request)
@ -246,7 +240,6 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
def form_valid(self, form):
# update notifications
comment = form.instance
del_mention_notifications(comment)
add_mention_notifications(comment)
with transaction.atomic(), revisions.create_revision():

View file

@ -27,6 +27,8 @@ from django.db.models import (
Value,
When,
)
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db.models.expressions import CombinedExpression
from django.http import (
Http404,
@ -67,6 +69,7 @@ from judge.models import (
Profile,
Submission,
ContestProblemClarification,
ContestsSummary,
)
from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status
@ -183,9 +186,16 @@ class ContestList(
self.request.GET.getlist("contest")
).strip()
if query:
queryset = queryset.filter(
substr_queryset = queryset.filter(
Q(key__icontains=query) | Q(name__icontains=query)
)
if settings.ENABLE_FTS:
queryset = (
queryset.search(query).extra(order_by=["-relevance"])
| substr_queryset
)
else:
queryset = substr_queryset
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
if self.show_orgs:
@ -226,9 +236,10 @@ class ContestList(
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"))
if not ("contest" in self.request.GET and settings.ENABLE_FTS):
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
@ -408,7 +419,14 @@ class ContestDetail(
return []
res = []
for organization in self.object.organizations.all():
can_edit = False
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)
return res
@ -430,16 +448,32 @@ class ContestDetail(
.add_i18n_name(self.request.LANGUAGE_CODE)
)
context["editable_organizations"] = self.get_editable_organizations()
context["is_clonable"] = is_contest_clonable(self.request, self.object)
return context
class ContestClone(
ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView
):
def is_contest_clonable(request, contest):
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")
template_name = "contest/clone.html"
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):
kwargs = super().get_form_kwargs()
@ -464,6 +498,7 @@ class ContestClone(
contest.is_visible = False
contest.user_count = 0
contest.key = form.cleaned_data["key"]
contest.is_rated = False
contest.save()
contest.tags.set(tags)
@ -1380,3 +1415,65 @@ def update_contest_mode(request):
old_mode = request.session.get("contest_mode", True)
request.session["contest_mode"] = not old_mode
return HttpResponse()
ContestsSummaryData = namedtuple(
"ContestsSummaryData",
"user points point_contests css_class",
)
def contests_summary_view(request, key):
try:
contests_summary = ContestsSummary.objects.get(key=key)
except:
raise Http404()
cache_key = "csv:" + key
context = cache.get(cache_key)
if context:
return render(request, "contest/contests_summary.html", context)
scores_system = contests_summary.scores
contests = contests_summary.contests.all()
total_points = defaultdict(int)
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
user_css_class = {}
for i in range(len(contests)):
contest = contests[i]
users, problems = get_contest_ranking_list(request, contest)
for rank, user in users:
curr_score = 0
if rank - 1 < len(scores_system):
curr_score = scores_system[rank - 1]
total_points[user.user] += curr_score
result_per_contest[user.user][i] = (curr_score, rank)
user_css_class[user.user] = user.css_class
sorted_total_points = [
ContestsSummaryData(
user=user,
points=total_points[user],
point_contests=result_per_contest[user],
css_class=user_css_class[user],
)
for user in total_points
]
sorted_total_points.sort(key=lambda x: x.points, reverse=True)
total_rank = ranker(sorted_total_points)
context = {
"total_rank": list(total_rank),
"title": _("Contests Summary"),
"contests": contests,
}
cache.set(cache_key, context)
return render(request, "contest/contests_summary.html", context)
@receiver([post_save, post_delete], sender=ContestsSummary)
def clear_cache(sender, instance, **kwargs):
cache.delete("csv:" + instance.key)

View file

@ -6,6 +6,7 @@ from django.utils.translation import gettext as _, gettext_lazy
from django.db.models import Count, Q
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.shortcuts import render
from judge.utils.diggpaginator import DiggPaginator
from judge.models import VolunteerProblemVote, Problem
@ -21,7 +22,7 @@ class InternalView(object):
class InternalProblem(InternalView, ListView):
model = Problem
title = _("Internal problems")
template_name = "internal/problem.html"
template_name = "internal/problem/problem.html"
paginate_by = 100
context_object_name = "problems"
@ -63,6 +64,28 @@ class InternalProblem(InternalView, ListView):
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):
def get_requests_data(self):
logger = logging.getLogger(self.log_name)

View file

@ -0,0 +1,14 @@
from django.views import View
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
class MarkdownEditor(View):
def get(self, request):
return render(
request,
"markdown_editor/markdown_editor.html",
{
"title": _("Markdown Editor"),
},
)

View file

@ -2,10 +2,9 @@ from django.contrib.auth.decorators import login_required
from django.views.generic import ListView
from django.utils.translation import ugettext as _
from django.utils.timezone import now
from django.db.models import BooleanField, Value
from judge.utils.cachedict import CacheDict
from judge.models import Profile, Comment, Notification
from judge.models import Profile, Notification, NotificationProfile
from judge.models.notification import unseen_notifications_count
__all__ = ["NotificationList"]
@ -16,24 +15,11 @@ class NotificationList(ListView):
template_name = "notification/list.html"
def get_queryset(self):
self.unseen_cnt = self.request.profile.count_unseen_notifications
self.unseen_cnt = unseen_notifications_count(self.request.profile)
query = {
"owner": self.request.profile,
}
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:
q.seen = False
else:
break
self.queryset = Notification.objects.filter(
owner=self.request.profile
).order_by("-id")[:100]
return self.queryset
@ -46,8 +32,6 @@ class NotificationList(ListView):
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)
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
unseen_notifications_count.dirty(self.request.profile)
return ret

View file

@ -56,10 +56,10 @@ from judge.models import (
Problem,
Profile,
Contest,
Notification,
ContestProblem,
OrganizationProfile,
)
from judge.models.notification import make_notification
from judge import event_poster as event
from judge.utils.ranker import ranker
from judge.utils.views import (
@ -73,6 +73,7 @@ from judge.views.problem import ProblemList
from judge.views.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.feed import FeedView
from judge.tasks import rescore_contest
__all__ = [
"OrganizationList",
@ -394,7 +395,7 @@ class OrganizationContestMixin(
model = 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
)
@ -947,7 +948,7 @@ class EditOrganizationContest(
def get_content_title(self):
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):
return self.contest
@ -960,6 +961,19 @@ class EditOrganizationContest(
self.object.organizations.add(self.organization)
self.object.is_organization_private = True
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
def get_problem_formset(self, post=False):
@ -1019,16 +1033,9 @@ class AddOrganizationBlog(
html = (
f'<a href="{link}">{self.object.title} - {self.organization.name}</a>'
)
for user in self.organization.admins.all():
if user.id == self.request.profile.id:
continue
notification = Notification(
owner=user,
author=self.request.profile,
category="Add blog",
html_link=html,
)
notification.save()
make_notification(
self.organization.admins.all(), "Add blog", html, self.request.profile
)
return res
@ -1104,17 +1111,8 @@ class EditOrganizationBlog(
)
html = f'<a href="{link}">{blog.title} - {self.organization.name}</a>'
post_authors = blog.authors.all()
posible_user = self.organization.admins.all() | post_authors
for user in posible_user:
if user.id == self.request.profile.id:
continue
notification = Notification(
owner=user,
author=self.request.profile,
category=action,
html_link=html,
)
notification.save()
posible_users = self.organization.admins.all() | post_authors
make_notification(posible_users, action, html, self.request.profile)
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():

View file

@ -80,6 +80,7 @@ def vote_page(request, delta):
else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
break
_dirty_vote_score(pagevote_id, request.profile)
return HttpResponse("success", content_type="text/plain")
@ -103,3 +104,8 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
context["pagevote"] = self.object.get_or_create_pagevote()
return context
def _dirty_vote_score(pagevote_id, profile):
pv = PageVote(id=pagevote_id)
pv.vote_score.dirty(pv, profile)

View file

@ -1,10 +1,8 @@
import logging
import os
import shutil
from datetime import timedelta, datetime
from operator import itemgetter
from random import randrange
import random
from copy import deepcopy
from django.core.cache import cache
@ -77,6 +75,8 @@ from judge.utils.problems import (
user_attempted_ids,
user_completed_ids,
get_related_problems,
get_user_recommended_problems,
RecommendationType,
)
from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter
@ -466,10 +466,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
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"
first_page_href = None
filter_organization = False
def get_default_sort_order(self, request):
if "search" in request.GET and settings.ENABLE_FTS:
return "-relevance"
return "-date"
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
@ -485,42 +489,46 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
)
if not self.in_contest:
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
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":
if self.request.user.is_authenticated:
profile = self.request.profile
solved = user_completed_ids(profile)
attempted = user_attempted_ids(profile)
def _solved_sort_order(problem):
if problem.id in solved:
return 1
if problem.id in attempted:
return 0
return -1
queryset = list(queryset)
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 = self.sort_queryset(queryset)
paginator.object_list = queryset
return paginator
def sort_queryset(self, queryset):
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":
if self.request.user.is_authenticated:
profile = self.request.profile
solved = user_completed_ids(profile)
attempted = user_attempted_ids(profile)
def _solved_sort_order(problem):
if problem.id in solved:
return 1
if problem.id in attempted:
return 0
return -1
queryset = list(queryset)
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("-"),
)
return queryset
@cached_property
def profile(self):
if not self.request.user.is_authenticated:
@ -611,36 +619,28 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
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"]
substr_queryset = queryset.filter(
Q(code__icontains=query)
| Q(name__icontains=query)
| Q(
translations__name__icontains=query,
translations__language=self.request.LANGUAGE_CODE,
)
)
if settings.ENABLE_FTS:
queryset = (
queryset.search(query, queryset.BOOLEAN).extra(
order_by=["-relevance"]
)
| substr_queryset
)
else:
queryset = queryset.filter(
Q(code__icontains=query)
| Q(name__icontains=query)
| Q(
translations__name__icontains=query,
translations__language=self.request.LANGUAGE_CODE,
)
)
queryset = substr_queryset
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=Case(
When(
solution__is_public=True,
solution__publish_on__lte=timezone.now(),
then=True,
),
default=False,
output_field=BooleanField(),
)
)
return queryset.distinct()
def get_queryset(self):
@ -658,7 +658,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
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["show_solved_only"] = (
0 if self.in_contest else int(self.show_solved_only)
)
@ -768,7 +767,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
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.show_solved_only = self.GET_with_session(request, "show_solved_only")
self.search_query = None
@ -816,7 +814,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
"show_types",
"full_text",
"show_editorial",
"have_editorial",
"show_solved_only",
)
for key in to_update:
@ -837,24 +834,34 @@ class ProblemFeed(ProblemList, FeedView):
title = _("Problem feed")
feed_type = None
# arr = [[], [], ..]
def merge_recommendation(self, arr):
seed = datetime.now().strftime("%d%m%Y")
merged_array = []
for a in arr:
merged_array += a
random.Random(seed).shuffle(merged_array)
def get_recommended_problem_ids(self, queryset):
user_id = self.request.profile.id
problem_ids = queryset.values_list("id", flat=True)
rec_types = [
RecommendationType.CF_DOT,
RecommendationType.CF_COSINE,
RecommendationType.CF_TIME_DOT,
RecommendationType.CF_TIME_COSINE,
RecommendationType.HOT_PROBLEM,
]
limits = [100, 100, 100, 100, 20]
shuffle = True
res = []
used_pid = set()
allow_debug_type = (
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:
if type(obj) == tuple:
obj = obj[1]
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res
return get_user_recommended_problems(
user_id, problem_ids, rec_types, limits, shuffle
)
def get_queryset(self):
if self.feed_type == "volunteer":
@ -862,9 +869,6 @@ class ProblemFeed(ProblemList, FeedView):
self.show_types = 1
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":
@ -886,43 +890,13 @@ class ProblemFeed(ProblemList, FeedView):
.order_by("?")
.add_i18n_name(self.request.LANGUAGE_CODE)
)
if "search" in self.request.GET:
return queryset.add_i18n_name(self.request.LANGUAGE_CODE)
if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
cf_model = CollabFilter("collab_filter")
cf_time_model = CollabFilter("collab_filter_time")
q = self.get_recommended_problem_ids(queryset)
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 = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
@ -946,7 +920,6 @@ class ProblemFeed(ProblemList, FeedView):
context["title"] = self.title
context["feed_type"] = self.feed_type
context["has_show_editorial_option"] = False
context["has_have_editorial_option"] = False
return context

View file

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

View file

@ -31,10 +31,9 @@ class Resolver(TemplateView):
for participation in self.contest.users.filter(virtual=0):
cnt_user += 1
users[str(cnt_user)] = {
"username": participation.user.user.username,
"name": participation.user.user.first_name
or participation.user.user.username,
"school": participation.user.user.last_name,
"username": participation.user.username,
"name": participation.user.first_name or participation.user.username,
"school": participation.user.last_name,
"last_submission": participation.cumtime_final,
"problems": {},
}

View file

@ -1,6 +1,5 @@
import json
import os.path
import zipfile
from operator import attrgetter
from django.conf import settings
@ -84,31 +83,7 @@ class SubmissionMixin(object):
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
def get_object(self, queryset=None):
submission = super(SubmissionDetailBase, self).get_object(queryset)
profile = self.request.profile
problem = submission.problem
if self.request.user.has_perm("judge.view_all_submission"):
return submission
if problem.is_public and self.request.user.has_perm(
"judge.view_public_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():
return submission
if hasattr(
submission, "contest"
) and submission.contest.participation.contest.is_editable_by(
self.request.user
):
if submission.is_accessible_by(self.request.profile):
return submission
raise PermissionDenied()
@ -220,8 +195,8 @@ def get_cases_data(submission):
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.get(case.input_file, "") if case.input_file else "",
"answer": case_data.get(case.output_file, "") if case.output_file else "",
}
return problem_data
@ -483,19 +458,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
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["editable_problem_ids"] = (
user_editable_ids(self.request.profile) if authenticated else []
)
context["tester_problem_ids"] = (
user_tester_ids(self.request.profile) if authenticated else []
)
context["profile"] = self.request.profile
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
@ -779,19 +744,10 @@ def single_submission(request, submission_id, show_problem=True):
"submission/row.html",
{
"submission": submission,
"completed_problem_ids": user_completed_ids(request.profile)
if authenticated
else [],
"editable_problem_ids": user_editable_ids(request.profile)
if authenticated
else [],
"tester_problem_ids": user_tester_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,
"profile": request.profile if authenticated else None,
},
)
@ -1010,9 +966,6 @@ class UserContestSubmissionsAjax(UserContestSubmissions):
context["contest"] = self.contest
context["problem"] = self.problem
context["profile"] = self.profile
context["profile_id"] = (
self.request.profile.id if self.request.profile else None
)
contest_problem = self.contest.contest_problems.get(problem=self.problem)
filtered_submissions = []

View file

@ -35,6 +35,7 @@ from judge.utils.tickets import filter_visible_tickets, own_ticket_filter
from judge.utils.views import SingleObjectFormView, TitleMixin, paginate_query_context
from judge.views.problem import ProblemMixin
from judge.widgets import HeavyPreviewPageDownWidget
from judge.models.notification import make_notification
ticket_widget = (
forms.Textarea()
@ -49,16 +50,10 @@ ticket_widget = (
def add_ticket_notifications(users, author, link, ticket):
html = f'<a href="{link}">{ticket.linked_item}</a>'
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.save()
make_notification(users, "Ticket", html, author)
class TicketForm(forms.Form):

View file

@ -52,11 +52,11 @@ else:
class Media:
css = {
"all": [
"markdown.css",
"pagedown_widget.css",
"content-description.css",
"admin/css/pagedown.css",
"pagedown.css",
"https://fonts.googleapis.com/css2?family=Fira+Code&family=Noto+Sans&display=swap",
]
}
js = ["admin/js/pagedown.js"]

File diff suppressed because it is too large Load diff

View file

@ -593,3 +593,9 @@ msgstr ""
msgid "z-function"
msgstr ""
#~ msgid "Insert Image"
#~ msgstr "Chèn hình ảnh"
#~ msgid "Save"
#~ msgstr "Lưu"

View file

@ -2,9 +2,12 @@
padding-right: 15px !important;
}
.wmd-preview {
margin-top: 15px;
padding: 15px;
word-wrap: break-word;
}
.md-typeset, .wmd-input {
line-height: 1.4em !important;
}

View file

@ -147,8 +147,9 @@
}
}
.blog-box:hover {
.blog-box:hover, .blog-box:not(.pre-expand-blog) {
border-color: #8a8a8a;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
}
.blog-description {
@ -229,7 +230,6 @@
.show-more {
display: flex;
color: black;
font-style: italic;
font-size: 16px;
font-weight: 700;
padding: 0px 12px;

View file

@ -40,6 +40,8 @@ a {
.comment-img {
display: flex;
margin-right: 4px;
height: 1.5em;
width: 1.5em;
}
.new-comments .comment-display {
@ -214,7 +216,7 @@ a {
input, textarea {
min-width: 100%;
max-width: 100%;
font-size: 1em;
font-size: 15px;
}
}
@ -267,10 +269,10 @@ a {
.actionbar-button {
cursor: pointer;
padding: 0.8em;
border: 0.2px solid lightgray;
border-radius: 5em;
font-weight: bold;
display: inherit;
background: lightgray;
}
.actionbar-block {
display: flex;
@ -285,7 +287,7 @@ a {
border-radius: 5em 0 0 5em;
}
.actionbar-button:hover {
background: lightgray;
background: darkgray;
}
.dislike-button {
padding-left: 0.5em;

View file

@ -387,7 +387,7 @@ function onWindowReady() {
});
$('a').click(function() {
var href = $(this).attr('href');
if (href === '#' || href.startsWith("javascript")) {
if (!href || href === '#' || href.startsWith("javascript")) {
return;
}

File diff suppressed because one or more lines are too long

View file

@ -1956,8 +1956,10 @@ input::placeholder {
background-color: rgb(24, 26, 27);
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);
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 2px;
}
.problem-feed-name a {
color: rgb(102, 177, 250);
@ -2471,16 +2473,13 @@ input[type="text"], input[type="password"], input[type="email"], input[type="num
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset;
}
textarea {
color: rgb(178, 172, 162);
background-image: none;
background-color: rgb(24, 26, 27);
border-color: rgb(62, 68, 70);
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset;
}
textarea:hover {
border-color: rgba(16, 87, 144, 0.8);
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset,
rgba(16, 91, 150, 0.6) 0px 0px 4px;
border-color: rgb(140, 130, 115);
}
input[type="text"]:hover, input[type="password"]:hover {
border-color: rgba(16, 87, 144, 0.8);
@ -2488,9 +2487,7 @@ input[type="text"]:hover, input[type="password"]:hover {
rgba(16, 91, 150, 0.6) 0px 0px 4px;
}
textarea:focus {
border-color: rgba(16, 87, 144, 0.8);
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset,
rgba(16, 91, 150, 0.6) 0px 0px 8px; outline-color: initial;
border-color: rgb(140, 130, 115); outline-color: initial;
}
input[type="text"]:focus, input[type="password"]:focus {
border-color: rgba(16, 87, 144, 0.8);
@ -2552,7 +2549,7 @@ input[type="text"]:focus, input[type="password"]:focus {
ul.pagination a:hover {
color: rgb(232, 230, 227);
background-image: initial;
background-color: rgb(8, 128, 104);
background-color: rgb(163, 62, 18);
border-color: initial;
}
ul.pagination > li > a,
@ -2563,14 +2560,14 @@ ul.pagination > li > span {
border-color: rgb(199, 70, 8);
}
ul.pagination > .disabled-page > a {
color: rgb(157, 148, 136);
background-color: rgba(3, 66, 54, 0.5);
border-color: rgba(126, 117, 103, 0.5);
color: rgb(223, 220, 215);
background-color: rgb(137, 78, 57);
border-color: rgb(199, 68, 21);
}
ul.pagination > .disabled-page > span {
color: rgb(157, 148, 136);
background-color: rgba(3, 66, 54, 0.5);
border-color: rgba(126, 117, 103, 0.5);
color: rgb(223, 220, 215);
background-color: rgb(137, 78, 57);
border-color: rgb(199, 68, 21);
}
ul.pagination > .active-page > a {
color: rgb(232, 230, 227);
@ -2785,11 +2782,15 @@ a.voted {
border-left-color: rgb(48, 52, 54);
}
.actionbar .actionbar-button {
border-color: rgb(60, 65, 68);
background-image: initial;
background-color: rgb(49, 53, 55);
}
.actionbar .actionbar-button a:hover {
color: inherit;
}
.actionbar .actionbar-button:hover {
background-image: initial;
background-color: rgb(49, 53, 55);
background-color: rgb(73, 79, 82);
}
.actionbar .dislike-button {
border-left-color: initial;

View file

@ -13,7 +13,7 @@ div.dmmd-preview-update {
}
div.dmmd-preview-content {
padding: 0 7px;
padding: 0 8px;
}
div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update {
@ -21,7 +21,8 @@ div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update {
}
div.dmmd-preview-has-content div.dmmd-preview-content {
padding-bottom: 7px;
padding-bottom: 8px;
padding-top: 8px;
}
div.dmmd-no-button div.dmmd-preview-update {

View file

@ -25,17 +25,17 @@ div.dmmd-preview-has-content div.dmmd-preview-content {
}
div.dmmd-no-button div.dmmd-preview-update {
display: none;
display: none;
}
div.dmmd-no-button div.dmmd-preview-content {
padding-bottom: 0;
padding-bottom: 0;
}
div.dmmd-no-button:not(.dmmd-preview-has-content) {
display: none;
display: none;
}
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.
-->
<script type="text/template" id="qq-template">
<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 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 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 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>
</script>

View file

@ -5,78 +5,78 @@
on how to customize this 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-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>
<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 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 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>
</script>

View file

@ -5,60 +5,60 @@
on how to customize this 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-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>
<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 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 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>
</script>

View file

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

View file

@ -14,7 +14,7 @@
width: 100%;
background: #fff;
border: 1px solid DarkGray;
font-family: Consolas, "Liberation Mono", Monaco, "Courier New", monospace !important;
font-family: "Noto Sans",Arial,"Lucida Grande",sans-serif !important;
}
.wmd-preview {

View file

@ -14,7 +14,7 @@
width: 100%;
background: #fff;
border: 1px solid DarkGray;
font-family: Consolas, "Liberation Mono", Monaco, "Courier New", monospace !important;
font-family: "Noto Sans",Arial,"Lucida Grande",sans-serif !important;
}
.wmd-preview {
@ -174,51 +174,51 @@
/* Extra styles to allow for image upload */
.pagedown-image-upload {
display: none;
z-index: 10001;
position: fixed;
background: white;
top: 50%;
left: 50%;
padding: 10px;
width: 400px;
max-width: 90%;
transform: translate3d(-50%, -50%, 0);
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
display: none;
z-index: 10001;
position: fixed;
background: white;
top: 50%;
left: 50%;
padding: 10px;
width: 400px;
max-width: 90%;
transform: translate3d(-50%, -50%, 0);
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
}
.pagedown-image-upload .submit-row {
margin: 10px 0 0 0;
margin: 10px 0 0 0;
}
.pagedown-image-upload.show {
display: block;
display: block;
}
.pagedown-image-upload .submit-loading {
display: none;
vertical-align: middle;
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #79aec8; /* Blue */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
display: none;
vertical-align: middle;
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #79aec8; /* Blue */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
.pagedown-image-upload .submit-loading.show {
display: inline-block;
display: inline-block;
}
.pagedown-image-upload .submit-input {
display: none;
display: none;
}
.pagedown-image-upload .submit-input.show {
display: inline-block;
display: inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View file

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

View file

@ -161,8 +161,7 @@ input {
}
textarea {
padding: 4px 8px;
color: #555;
padding: 8px;
background: #FFF none;
border: 1px solid $border_gray;
border-radius: $widget_border_radius;
@ -172,8 +171,7 @@ textarea {
}
textarea:hover {
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 4px rgba(82, 168, 236, 0.6);
border-color: black;
}
input {
@ -184,8 +182,8 @@ input {
}
textarea:focus {
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
border-color: black;
border-width: unset;
outline: 0;
}
@ -322,7 +320,7 @@ input {
// Bootstrap-y pagination
ul.pagination a:hover {
color: #FFF;
background: #0aa082;
background: #cc4e17;
border: none;
}
@ -338,22 +336,6 @@ ul.pagination {
li {
display: inline;
// &:first-child > {
// a, span {
// margin-left: 0;
// border-top-left-radius: $widget_border_radius;
// border-bottom-left-radius: $widget_border_radius;
// }
// }
// &:last-child > {
// a, span {
// margin-left: 0;
// border-top-right-radius: $widget_border_radius;
// border-bottom-right-radius: $widget_border_radius;
// }
// }
> {
a, span {
position: relative;
@ -373,15 +355,15 @@ ul.pagination {
.disabled-page > {
a {
color: #888;
background-color: #04534380;
border-color: #04534380;
color: #f1efef;
background-color: #ab6247;
border-color: #6a240b;
}
span {
color: #888;
background-color: #04534380;
border-color: #04534380;
color: #f1efef;
background-color: #ab6247;
border-color: #6a240b;
}
}

View file

@ -18,13 +18,20 @@
width: 100% !important;
}
#content input.select2-search__field {
border: none;
box-shadow: none !important;
}
#content .content-description h1,
#content .content-description h2,
#content .content-description h3,
#content .content-description h4,
#content .content-description h5,
#content .content-description h6 {
padding: 0;
padding-left: 0;
padding-right: 0;
font-size: inherit;
}
#content .content-description h5 {
@ -32,14 +39,16 @@
text-transform: initial;
}
#content input.select2-search__field {
border: none;
box-shadow: none !important;
#content .content-description ul,
#content .content-description ol {
margin-left: 0;
margin-right: 0;
}
#content .content-description ul,
#content .content-description ol {
margin: 0;
margin-left: 0;
margin-right: 0;
}
#content .content-description li {

View file

@ -113,7 +113,7 @@
navigator.clipboard
.writeText(link)
.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') }}">
{% endcompress %}
{% 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>
<style>
@ -378,12 +272,121 @@
<div id="announcement">{{ i18n_config.announcement|safe }}</div>
{% 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 footer %}
<footer>
<span id="footer-content">
<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 %}
{{ i18n_config.footer|safe }} |
{% endif %}

View file

@ -5,7 +5,10 @@
{% block title %} {{_('Chat Box')}} {% endblock %}
{% 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="module" src="https://unpkg.com/emoji-picker-element@1"></script>
{% compress js %}
@ -79,7 +82,7 @@
{% include 'chat/user_online_status.html' %}
</div>
<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">
{% include 'chat/message_list.html' %}
</ul>

View file

@ -10,7 +10,15 @@
}
::-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 {
@ -144,9 +152,11 @@
transition: box-shadow 0.3s ease-in-out;
width: 80%;
resize: none;
height: 80%;
height: 70%;
max-height: 200px;
overflow-y: auto;
margin-top: auto;
margin-bottom: 6px;
}
#chat-input:focus {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);

View file

@ -1,10 +1,4 @@
<script type="text/javascript">
let META_HEADER = [
"{{_('Recent')}}",
"{{_('Following')}}",
"{{_('Admin')}}",
"{{_('Other')}}",
];
let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches;
function load_next_page(last_id, refresh_html=false) {
@ -561,7 +555,7 @@
this.style.height = (this.scrollHeight) + 'px';
$(this).css('border-radius', '30px');
} 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 }}">
<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">
</a>
<div class="body-message">
<div class="user-time">
<span class="username {{ message.author.css_class }}">
<a href="{{ url('user_page', message.author.user.username) }}">
{{ message.author }}
<a href="{{ url('user_page', message.author.username) }}">
{{ message.author.username }}
</a>
</span>
<span class="time">

View file

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

View file

@ -1,13 +1,10 @@
{% set logged_in = request.user.is_authenticated %}
{% set profile = request.profile if logged_in else None %}
{% for node in mptt_tree(comment_list) recursive %}
<li id="comment-{{ node.id }}" data-revision="{{ node.revisions - 1 }}" data-max-revision="{{ node.revisions - 1 }}"
data-revision-ajax="{{ url('comment_revision_ajax', node.id) }}" class="comment">
<div class="comment-display{% if node.score <= vote_hide_threshold %} bad-comment{% endif %}">
<div class="info">
<div class="vote">
{% if logged_in %}
{% if profile %}
<a href="javascript:comment_upvote({{ node.id }})"
class="upvote-link fa fa-chevron-up fa-fw{% if node.vote_score == 1 %} voted{% endif %}"></a>
{% else %}
@ -16,7 +13,7 @@
{% endif %}
<br>
<div class="comment-score">{{ node.score }}</div>
{% if logged_in %}
{% if profile %}
<a href="javascript:comment_downvote({{ node.id }})"
class="downvote-link fa fa-chevron-down fa-fw{% if node.vote_score == -1 %} voted{% endif %}"></a>
{% else %}
@ -55,7 +52,7 @@
<a href="?comment-id={{node.id}}#comment-{{ node.id }}" title="{{ _('Link') }}" class="comment-link">
<i class="fa fa-link fa-fw"></i>
</a>
{% if logged_in and not comment_lock %}
{% if profile and not comment_lock %}
{% set can_edit = node.author.id == profile.id and not profile.mute %}
{% if can_edit %}
<a data-featherlight="{{ url('comment_edit_ajax', node.id) }}"

View file

@ -30,7 +30,7 @@
{% endif %}
<script type="text/javascript">
$(document).ready(function () {
let loading_gif = "<img src=\"{{static('loading.gif')}}\" style=\"height: 1.5em; margin-bottom: 3px\" class=\"loading\">";
let loading_gif = "<img src=\"{{static('loading.gif')}}\" style=\"height: 3em; margin-bottom: 3px\" class=\"loading\">";
window.reply_comment = function (parent) {
var $comment_reply = $('#comment-' + parent + '-reply');
var reply_id = 'reply-' + parent;
@ -145,6 +145,8 @@
$comment_loading.hide();
var $comment = $("#comment-" + id + "-children");
$comment.append(data);
MathJax.typeset($('#comments')[0]);
register_time($('.time-with-rel'));
}
})
}
@ -187,6 +189,7 @@
$comment.append(data);
}
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 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}}
{% if form.organization.errors %}
<div id="form-errors">

View file

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

View file

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

View file

@ -0,0 +1,71 @@
{% extends "base.html" %}
{% block title_row %}{% endblock %}
{% block title_ruler %}{% endblock %}
{% block media %}
<style>
table {
width: 100%;
border-collapse: collapse;
border: 1px solid #ddd;
font-size: 1em;
}
th, td {
padding: 12px 15px;
border: 1px solid #595656;
padding: 15px;
text-align: center;
}
th {
font-size: 15px;
}
tr:hover {
cursor: pointer;
}
</style>
{% endblock %}
{% block body %}
<table class="table" id="users-table">
<thead>
<tr>
<th>{{_('Rank')}}</th>
<th>{{_('Name')}}</th>
{% for contest in contests %}
<th><a href="{{url('contest_view', contest.key)}}" title="{{contest.name}}">{{ loop.index }}</a></th>
{% endfor %}
<th>{{_('Points')}}</th>
</tr>
</thead>
<tbody>
{% for rank, item in total_rank %}
<tr>
<td>
{{ rank }}
</td>
<td>
<div>
<span class="username {{ item.css_class }} wrapline" href="{{url('user_page', item.user.username)}}" >{{item.user.username}}</span>
</div>
<div>{{item.user.first_name}}</div>
</td>
{% for point_contest, rank_contest in item.point_contests %}
<td>
<div>{{ point_contest }}</div>
{% if rank_contest %}
<div><small>(#{{ rank_contest }})</small></div>
{% endif %}
</td>
{% endfor %}
<td>
<b>{{ item.points }}</b>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -112,56 +112,54 @@
{% endblock %}
{% macro contest_head(contest) %}
{% spaceless %}
<a href="{{ url('contest_view', contest.key) }}" class="contest-list-title" style="margin-right: 5px;">
{{- contest.name -}}
</a>
<br>
<div class="contest-tags" style="margin-top: 5px;">
{% if not contest.is_visible %}
<span class="contest-tag contest-tag-hidden">
<i class="fa fa-eye-slash"></i> {{ _('hidden') }}
</span>
<a href="{{ url('contest_view', contest.key) }}" class="contest-list-title" style="margin-right: 5px;">
{{contest.name}}
</a>
<br>
<div class="contest-tags" style="margin-top: 5px;">
{% if not contest.is_visible %}
<span class="contest-tag contest-tag-hidden">
<i class="fa fa-eye-slash"></i> {{ _('hidden') }}
</span>
{% endif %}
{% if contest.is_editable %}
<span class="contest-tag contest-tag-edit">
<a href="{{ url('organization_contest_edit', organization.id, organization.slug, contest.key) }}" class="white">
<i class="fa fa-edit"></i> {{ _('Edit') }}
</a>
</span>
{% endif %}
{% if contest.is_private %}
<span class="contest-tag contest-tag-private">
<i class="fa fa-lock"></i> {{ _('private') }}
</span>
{% endif %}
{% if not hide_contest_orgs %}
{% if contest.is_organization_private %}
{% for org in contest.organizations.all() %}
<span class="contest-tag contest-tag-org">
<a href="{{ org.get_absolute_url() }}">
<i class="fa fa-lock"></i> {{ org.name }}
</a>
</span>
{% endfor %}
{% endif %}
{% if contest.is_editable %}
<span class="contest-tag contest-tag-edit">
<a href="{{ url('organization_contest_edit', organization.id, organization.slug, contest.key) }}" class="white">
<i class="fa fa-edit"></i> {{ _('Edit') }}
</a>
</span>
{% endif %}
{% if contest.is_private %}
<span class="contest-tag contest-tag-private">
<i class="fa fa-lock"></i> {{ _('private') }}
</span>
{% endif %}
{% if not hide_contest_orgs %}
{% if contest.is_organization_private %}
{% for org in contest.organizations.all() %}
<span class="contest-tag contest-tag-org">
<a href="{{ org.get_absolute_url() }}">
<i class="fa fa-lock"></i> {{ org.name }}
</a>
</span>
{% endfor %}
{% endif %}
{% endif %}
{% if contest.is_rated %}
<span class="contest-tag contest-tag-rated">
<i class="fa fa-bar-chart"></i> {{ _('rated') }}
</span>
{% endif %}
{% for tag in contest.tags.all() %}
<span style="background-color: {{ tag.color }}" class="contest-tag">
<a href="{{ url('contest_tag', tag.name) }}"
style="color: {{ tag.text_color }}"
data-featherlight="{{ url('contest_tag_ajax', tag.name) }}">
{{- tag.name -}}
</a>
</span>
{% endfor %}
</div>
{% endspaceless %}
{% endif %}
{% if contest.is_rated %}
<span class="contest-tag contest-tag-rated">
<i class="fa fa-bar-chart"></i> {{ _('rated') }}
</span>
{% endif %}
{% for tag in contest.tags.all() %}
<span style="background-color: {{ tag.color }}" class="contest-tag">
<a href="{{ url('contest_tag', tag.name) }}"
style="color: {{ tag.text_color }}"
data-featherlight="{{ url('contest_tag_ajax', tag.name) }}">
{{- tag.name -}}
</a>
</span>
{% endfor %}
</div>
{% endmacro %}
{% macro time_left(contest, padding_top = true) %}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<div class="has_next" style="display: none;" value="{{1 if has_next_page else 0}}"></div>
{% if has_next_page %}
<button class="view-next-page btn-green small">{{_('View more')}}</button>
<button class="view-next-page">{{_('View more')}}</button>
{% endif %}

View file

@ -16,8 +16,9 @@
$('.vote-detail').each(function() {
$(this).on('click', function() {
var pid = $(this).attr('pid');
$('.detail').hide();
$('#detail-'+pid).show();
$.get("{{url('internal_problem_votes')}}?id="+pid, function(data) {
$('#detail').html(data);
});
})
})
});
@ -59,37 +60,8 @@
{% block right_sidebar %}
<div style="display: block; width: 100%">
<div><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('Admin')}}</a></div>
{% for problem in problems %}
<div class="detail" id="detail-{{problem.id}}" style="display: none;">
<h3>{{_('Votes for problem') }} {{problem.name}}</h3>
<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 %}
<a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('Admin')}}</a>
<div class="detail" id="detail">
</div>
</div>
{% 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

@ -0,0 +1,143 @@
{% extends "base.html" %}
{% block media %}
<style>
main{
overflow: hidden;
}
#content{
width: 100%;
}
#content.wrapper{
padding: 0;
}
.form-area{
float: left;
width: 47%;
height: 100%
}
.wmd-preview{
float: right;
margin-top: 0;
}
.width-controller{
width: 51%;
}
.wmd-input{
height: calc(100vh - 72px);
}
.right-markdown{
height: calc(100vh - 72px);
overflow-y: scroll;
}
.wrap{
display: flex;
}
</style>
{% endblock %}
{% block js_media %}
<script>
$(document).ready(function(){
$("#wmd-input-id_body").on("keyup", function() {
const csrfToken = "{{ csrf_token }}";
$.ajax({
url: "{{url('blog_preview')}}",
type: 'POST',
headers: {
'X-CSRFToken': csrfToken, // Include the CSRF token in the headers
},
data: {
preview: $(this).val()
},
success: function(data) {
$('#display').html(data);
MathJax.typeset();
},
error: function(error) {
alert(error);
console.log(error.message)
}
})
});
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const leftDiv = document.getElementById("wmd-input-id_body");
const rightDiv = document.getElementById("display");
leftDiv.addEventListener("scroll", function() {
rightDiv.scrollTop = leftDiv.scrollTop;
});
rightDiv.addEventListener("scroll", function() {
leftDiv.scrollTop = rightDiv.scrollTop;
});
});
</script>
<script src="{{ static('pagedown/Markdown.Converter.js') }}"></script>
<script src="{{ static('pagedown-extra/pagedown/Markdown.Converter.js') }}"></script>
<script src="{{ static('pagedown/Markdown.Sanitizer.js') }}"></script>
<script src="{{ static('pagedown/Markdown.Editor.js') }}"></script>
<script src="{{ static('pagedown-extra/Markdown.Extra.js') }}"></script>
<script src="{{ static('pagedown_init.js') }}"></script>
<script src="{{ static('mathjax3_config.js') }}"></script>
<script src="http://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
<script src="{{ static('pagedown_math.js') }}"></script>
{% endblock %}
{% block title_row %}
{% endblock %}
{% block title_ruler %}
{% endblock %}
{% block body %}
<div class="wrap">
<div id="new-comment" class="form-area">
<input type="hidden" name="parent" id="id_parent">
<div class="comment-post-wrapper">
<div id="comment-form-body"><div class="wmd-wrapper image-upload-enabled">
<div class="wmd-panel">
<div id="wmd-button-bar-id_body"></div>
<textarea id="wmd-input-id_body" class="wmd-input" name="body" required=""></textarea>
</div>
<div id="id_body-preview" data-preview-url="{{url('comment_preview')}}" data-textarea-id="wmd-input-id_body" data-timeout="1000" class="wmd-panel wmd-preview dmmd-preview dmmd-no-button">
<div class="dmmd-preview-update"><i class="fa fa-refresh"></i> {{_('Update Preview')}}</div>
<div class="dmmd-preview-content content-description"></div>
</div>
<div class="pagedown-image-upload">
<h2>{{_('Insert Image')}}</h2>
<div class="form-row">
<div>
<label class="label">{{_('From the web')}}</label>
<input class="url-input" type="text" placeholder="http://">
</div>
</div>
<div class="form-row">
<div>
<label class="label">{{_('From your computer')}}</label>
<input class="file-input" type="file" name="image" id="file" data-action="/pagedown/image-upload/" accept="image/*">
</div>
</div>
<div class="submit-row">
<div class="submit-loading"></div>
<input class="submit-input show" type="submit" value="{{_('Save')}}" name="_addanother">
<p class="deletelink-box"><a href="#" class="close-image-upload deletelink">{{_('Cancel')}}</a></p>
</div>
</div>
</div></div>
</div>
</div>
<div id="id_body-preview" data-preview-url="{{url('comment_preview')}}" data-textarea-id="wmd-input-id_body" data-timeout="1000" class="width-controller wmd-panel wmd-preview dmmd-preview dmmd-no-button dmmd-preview-has-content">
<div class="right-markdown dmmd-preview-content content-description" id="display"></div>
</div>
</div>
{% endblock %}

View file

@ -1,11 +1,8 @@
{% extends "base.html" %}
{% block body %}
{% if not has_notifications %}
<h2 style="text-align: center;">{{ _('You have no notifications') }}</h2>
{% else %}
<table class="table">
<tr>
@ -17,24 +14,15 @@
{% for notification in notifications %}
<tr class="{{ 'highlight' if not notification.seen }}">
<td>
{% if notification.comment %}
{{ link_user(notification.comment.author) }}
{% else %}
{{ link_user(notification.author) }}
{% endif %}
{{ link_user(notification.author) }}
</td>
<td>
{{ notification.category }}
</td>
<td>
{% if notification.comment %}
<a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ notification.comment.page_title }}</a>
{% else %}
{% autoescape off %}
{{notification.html_link}}
{% endautoescape %}
{% endif %}
{% autoescape off %}
{{notification.html_link}}
{% endautoescape %}
</td>
<td>
{{ relative_time(notification.time) }}
@ -43,8 +31,5 @@
{% endfor %}
</table>
{% endif %}
{% endblock %}
<!--
-->

View file

@ -29,12 +29,6 @@
height: 2em;
padding-top: 4px;
}
@media(min-width: 800px) {
#content {
width: 99%;
margin-left: 0;
}
}
@media(max-width: 799px) {
#content {
width: 100%;
@ -114,7 +108,7 @@
$('#go').click(clean_submit);
$('input#full_text, input#hide_solved, input#show_types, input#show_editorial, input#have_editorial, input#show_solved_only').click(function () {
$('input#full_text, input#hide_solved, input#show_types, input#have_editorial, input#show_solved_only').click(function () {
prep_form();
($('<form>').attr('action', window.location.pathname + '?' + form_serialize())
.append($('<input>').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken')

View file

@ -1,4 +1,4 @@
{% if related_problems %}
{% if request.user.is_authenticated and related_problems %}
<div class="content-description">
<h4>{{_('Recommended problems')}}:</h4>
<ul style="list-style-type: none; margin: 0.3em;">

View file

@ -34,13 +34,6 @@
<label for="show_editorial">{{ _('Show editorial') }}</label>
</div>
{% endif %}
{% if has_have_editorial_option %}
<div>
<input id="have_editorial" type="checkbox" name="have_editorial" value="1"
{% if have_editorial %} checked{% endif %}>
<label for="have_editorial">{{ _('Have editorial') }}</label>
</div>
{% endif %}
{% if organizations %}
<div class="filter-form-group">
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label>

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