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 rev: 22.12.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/hadialqattan/pycln
rev: 'v2.3.0'
hooks:
- id: pycln

View file

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

View file

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.db.models import CASCADE, Q from django.db.models import CASCADE, Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile from judge.models.profile import Profile
@ -17,22 +18,40 @@ class Room(models.Model):
user_two = models.ForeignKey( user_two = models.ForeignKey(
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
) )
last_msg_time = models.DateTimeField(
verbose_name=_("last seen"), null=True, db_index=True
)
class Meta:
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): 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): def other_user(self, profile):
return self.user_one if profile == self.user_two else self.user_two return self.user_one if profile == self.user_two else self.user_two
@cache_wrapper(prefix="Rus") def other_user_id(self, profile):
user_ids = self._cached_info["user_ids"]
return sum(user_ids) - profile.id
def users(self): def users(self):
return [self.user_one, self.user_two] return [self.user_one, self.user_two]
@cache_wrapper(prefix="Rlmb")
def last_message_body(self): def last_message_body(self):
return self.message_set.first().body return self._cached_info["last_message"]
class Message(models.Model): class Message(models.Model):
@ -58,6 +77,7 @@ class Message(models.Model):
indexes = [ indexes = [
models.Index(fields=["hidden", "room", "-id"]), models.Index(fields=["hidden", "room", "-id"]),
] ]
app_label = "chat_box"
class UserRoom(models.Model): class UserRoom(models.Model):
@ -70,6 +90,7 @@ class UserRoom(models.Model):
class Meta: class Meta:
unique_together = ("user", "room") unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model): class Ignore(models.Model):
@ -82,6 +103,9 @@ class Ignore(models.Model):
) )
ignored_users = models.ManyToManyField(Profile) ignored_users = models.ManyToManyField(Profile)
class Meta:
app_label = "chat_box"
@classmethod @classmethod
def is_ignored(self, current_user, new_friend): def is_ignored(self, current_user, new_friend):
try: try:

View file

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

View file

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

View file

@ -9,7 +9,6 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
""" """
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import datetime
import os import os
import tempfile import tempfile
@ -485,6 +484,9 @@ META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR"
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
try: try:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
exec(f.read(), globals()) exec(f.read(), globals())

View file

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

View file

@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin 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 ( from judge.admin.interface import (
BlogPostAdmin, BlogPostAdmin,
LicenseAdmin, LicenseAdmin,
@ -41,6 +46,7 @@ from judge.models import (
Ticket, Ticket,
VolunteerProblemVote, VolunteerProblemVote,
Course, Course,
ContestsSummary,
) )
@ -69,3 +75,4 @@ admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course) admin.site.register(Course)
admin.site.unregister(User) admin.site.unregister(User)
admin.site.register(User, UserAdmin) 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.short_description = _("virtual")
show_virtual.admin_order_field = "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, Solution,
Notification, Notification,
) )
from judge.models.notification import make_notification
from judge.widgets import ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget, AdminSelect2MultipleWidget,
@ -32,6 +33,7 @@ from judge.widgets import (
CheckboxSelectMultipleWithSelectAll, CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget, HeavyPreviewAdminPageDownWidget,
) )
from judge.utils.problems import user_editable_ids, user_tester_ids
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB")) MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
@ -358,12 +360,31 @@ class ProblemAdmin(CompareVersionAdmin):
self._rescore(request, obj.id) self._rescore(request, obj.id)
def save_related(self, request, form, formsets, change): 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) super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model`
obj = form.instance obj = form.instance
obj.curators.add(request.profile) obj.curators.add(request.profile)
obj.is_organization_private = obj.organizations.count() > 0 obj.is_organization_private = obj.organizations.count() > 0
obj.save() 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 # Create notification
if "is_public" in form.changed_data or "organizations" in form.changed_data: if "is_public" in form.changed_data or "organizations" in form.changed_data:
users = set(obj.authors.all()) users = set(obj.authors.all())
@ -381,14 +402,7 @@ class ProblemAdmin(CompareVersionAdmin):
category = "Problem public: " + str(obj.is_public) category = "Problem public: " + str(obj.is_public)
if orgs: if orgs:
category += " (" + ", ".join(orgs) + ")" category += " (" + ", ".join(orgs) + ")"
for user in users: make_notification(users, category, html, request.profile)
notification = Notification(
owner=user,
html_link=html,
category=category,
author=request.profile,
)
notification.save()
def construct_change_message(self, request, form, *args, **kwargs): def construct_change_message(self, request, form, *args, **kwargs):
if form.cleaned_data.get("change_message"): if form.cleaned_data.get("change_message"):

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -157,14 +157,14 @@ def item_title(item):
@registry.render_with("user/link.html") @registry.render_with("user/link.html")
def link_user(user): def link_user(user):
if isinstance(user, Profile): if isinstance(user, Profile):
user, profile = user.user, user profile = user
elif isinstance(user, AbstractUser): elif isinstance(user, AbstractUser):
profile = user.profile profile = user.profile
elif type(user).__name__ == "ContestRankingProfile": elif type(user).__name__ == "ContestRankingProfile":
user, profile = user.user, user profile = user
else: else:
raise ValueError("Expected profile or user, got %s" % (type(user),)) raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"user": user, "profile": profile} return {"profile": profile}
@registry.function @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 django.core.management.base import BaseCommand
from judge.models import * from judge.models import *
from collections import defaultdict
import csv import csv
import os import os
from django.conf import settings from django.conf import settings

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -177,29 +177,3 @@ class CommentLock(models.Model):
def __str__(self): def __str__(self):
return str(self.page) 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.ratings import rate_contest
from judge.models.pagevote import PageVotable from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable from judge.models.bookmark import Bookmarkable
from judge.fulltext import SearchManager
__all__ = [ __all__ = [
"Contest", "Contest",
@ -33,6 +34,7 @@ __all__ = [
"ContestSubmission", "ContestSubmission",
"Rating", "Rating",
"ContestProblemClarification", "ContestProblemClarification",
"ContestsSummary",
] ]
@ -97,11 +99,13 @@ class Contest(models.Model, PageVotable, Bookmarkable):
) )
authors = models.ManyToManyField( authors = models.ManyToManyField(
Profile, Profile,
verbose_name=_("authors"),
help_text=_("These users will be able to edit the contest."), help_text=_("These users will be able to edit the contest."),
related_name="authors+", related_name="authors+",
) )
curators = models.ManyToManyField( curators = models.ManyToManyField(
Profile, Profile,
verbose_name=_("curators"),
help_text=_( help_text=_(
"These users will be able to edit the contest, " "These users will be able to edit the contest, "
"but will not be listed as authors." "but will not be listed as authors."
@ -111,6 +115,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
) )
testers = models.ManyToManyField( testers = models.ManyToManyField(
Profile, Profile,
verbose_name=_("testers"),
help_text=_( help_text=_(
"These users will be able to view the contest, " "but not edit it." "These users will be able to view the contest, " "but not edit it."
), ),
@ -308,6 +313,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
comments = GenericRelation("Comment") comments = GenericRelation("Comment")
pagevote = GenericRelation("PageVote") pagevote = GenericRelation("PageVote")
bookmark = GenericRelation("BookMark") bookmark = GenericRelation("BookMark")
objects = SearchManager(("key", "name"))
@cached_property @cached_property
def format_class(self): def format_class(self):
@ -900,3 +906,27 @@ class ContestProblemClarification(models.Model):
date = models.DateTimeField( date = models.DateTimeField(
verbose_name=_("clarification timestamp"), auto_now_add=True 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 django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["PageVote", "PageVoteVoter"] __all__ = ["PageVote", "PageVoteVoter"]
@ -28,6 +29,7 @@ class PageVote(models.Model):
] ]
unique_together = ("content_type", "object_id") unique_together = ("content_type", "object_id")
@cache_wrapper(prefix="PVvs")
def vote_score(self, user): def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user) page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
if page_vote.exists(): if page_vote.exists():

View file

@ -1,6 +1,5 @@
import errno import errno
from operator import attrgetter from operator import attrgetter
from math import sqrt
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
@ -107,9 +106,7 @@ class License(models.Model):
class TranslatedProblemQuerySet(SearchQuerySet): class TranslatedProblemQuerySet(SearchQuerySet):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(TranslatedProblemQuerySet, self).__init__( super(TranslatedProblemQuerySet, self).__init__(("code", "name"), **kwargs)
("code", "name", "description"), **kwargs
)
def add_i18n_name(self, language): def add_i18n_name(self, language):
return self.annotate( return self.annotate(
@ -436,15 +433,23 @@ class Problem(models.Model, PageVotable, Bookmarkable):
@cached_property @cached_property
def author_ids(self): 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 @cached_property
def editor_ids(self): 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 @cached_property
def tester_ids(self): 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 @cached_property
def usable_common_names(self): def usable_common_names(self):
@ -551,7 +556,7 @@ class Problem(models.Model, PageVotable, Bookmarkable):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(Problem, self).save(*args, **kwargs) super(Problem, self).save(*args, **kwargs)
if self.code != self.__original_code: if self.__original_code and self.code != self.__original_code:
if hasattr(self, "data_files") or self.pdf_description: if hasattr(self, "data_files") or self.pdf_description:
try: try:
problem_data_storage.rename(self.__original_code, self.code) problem_data_storage.rename(self.__original_code, self.code)

View file

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

View file

@ -10,12 +10,17 @@ from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ 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 fernet_fields import EncryptedCharField
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
from judge.models.runtime import Language from judge.models.runtime import Language
from judge.ratings import rating_class from judge.ratings import rating_class
from judge.caching import cache_wrapper
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"] __all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
@ -142,6 +147,7 @@ class Organization(models.Model):
) )
verbose_name = _("organization") verbose_name = _("organization")
verbose_name_plural = _("organizations") verbose_name_plural = _("organizations")
app_label = "judge"
class Profile(models.Model): 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."), help_text=_("User will not be able to vote on problems' point values."),
default=False, default=False,
) )
rating = models.IntegerField(null=True, default=None) rating = models.IntegerField(null=True, default=None, db_index=True)
user_script = models.TextField( user_script = models.TextField(
verbose_name=_("user script"), verbose_name=_("user script"),
default="", default="",
@ -254,6 +260,24 @@ class Profile(models.Model):
max_length=300, 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 @cached_property
def organization(self): def organization(self):
# We do this to take advantage of prefetch_related # We do this to take advantage of prefetch_related
@ -262,14 +286,33 @@ class Profile(models.Model):
@cached_property @cached_property
def username(self): 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 @cached_property
def count_unseen_notifications(self): def count_unseen_notifications(self):
query = { from judge.models.notification import unseen_notifications_count
"read": False,
} return unseen_notifications_count(self)
return self.notifications.filter(**query).count()
@cached_property @cached_property
def count_unread_chat_boxes(self): def count_unread_chat_boxes(self):
@ -498,3 +541,21 @@ class OrganizationProfile(models.Model):
@classmethod @classmethod
def get_most_recent_organizations(self, users): def get_most_recent_organizations(self, users):
return self.objects.filter(users=users).order_by("-last_visit")[:5] 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): def id_secret(self):
return self.get_id_secret(self.id) 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: class Meta:
permissions = ( permissions = (
("abort_any_submission", "Abort any submission"), ("abort_any_submission", "Abort any submission"),

View file

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

View file

@ -5,7 +5,6 @@ from django import forms
from django.forms import ClearableFileInput from django.forms import ClearableFileInput
import os, os.path import os, os.path
import tempfile
import shutil import shutil
__all__ = ("handle_upload", "save_upload", "FineUploadForm", "FineUploadFileInput") __all__ = ("handle_upload", "save_upload", "FineUploadForm", "FineUploadFileInput")
@ -35,7 +34,7 @@ def save_upload(f, path):
# pass callback function to post_upload # pass callback function to post_upload
def handle_upload(f, fileattrs, upload_dir, post_upload=None): 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)): if not os.path.exists(os.path.dirname(chunks_dir)):
os.makedirs(os.path.dirname(chunks_dir)) os.makedirs(os.path.dirname(chunks_dir))
chunked = False chunked = False

View file

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

View file

@ -1,8 +1,8 @@
from collections import defaultdict from collections import defaultdict
from math import e from math import e
import os, zipfile from datetime import datetime, timedelta
from datetime import datetime
import random import random
from enum import Enum
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -10,6 +10,7 @@ from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When
from django.db.models.fields import FloatField from django.db.models.fields import FloatField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _, gettext_noop from django.utils.translation import gettext as _, gettext_noop
from django.http import Http404
from judge.models import Problem, Submission from judge.models import Problem, Submission
from judge.ml.collab_filter import CollabFilter from judge.ml.collab_filter import CollabFilter
@ -24,40 +25,41 @@ __all__ = [
] ]
@cache_wrapper(prefix="user_tester")
def user_tester_ids(profile): def user_tester_ids(profile):
return set( return set(
Problem.testers.through.objects.filter(profile=profile).values_list( Problem.testers.through.objects.filter(profile=profile)
"problem_id", flat=True .values_list("problem_id", flat=True)
) .distinct()
) )
@cache_wrapper(prefix="user_editable")
def user_editable_ids(profile): def user_editable_ids(profile):
result = set( result = set(
( (
Problem.objects.filter(authors=profile) Problem.objects.filter(authors=profile)
| Problem.objects.filter(curators=profile) | Problem.objects.filter(curators=profile)
).values_list("id", flat=True) )
.values_list("id", flat=True)
.distinct()
) )
return result return result
@cache_wrapper(prefix="contest_complete")
def contest_completed_ids(participation): def contest_completed_ids(participation):
key = "contest_complete:%d" % participation.id result = set(
result = cache.get(key) participation.submissions.filter(
if result is None: submission__result="AC", points=F("problem__points")
result = set(
participation.submissions.filter(
submission__result="AC", points=F("problem__points")
)
.values_list("problem__problem__id", flat=True)
.distinct()
) )
cache.set(key, result, 86400) .values_list("problem__problem__id", flat=True)
.distinct()
)
return result return result
@cache_wrapper(prefix="user_complete", timeout=86400) @cache_wrapper(prefix="user_complete")
def user_completed_ids(profile): def user_completed_ids(profile):
result = set( result = set(
Submission.objects.filter( Submission.objects.filter(
@ -69,7 +71,7 @@ def user_completed_ids(profile):
return result return result
@cache_wrapper(prefix="contest_attempted", timeout=86400) @cache_wrapper(prefix="contest_attempted")
def contest_attempted_ids(participation): def contest_attempted_ids(participation):
result = { result = {
id: {"achieved_points": points, "max_points": max_points} id: {"achieved_points": points, "max_points": max_points}
@ -84,7 +86,7 @@ def contest_attempted_ids(participation):
return result return result
@cache_wrapper(prefix="user_attempted", timeout=86400) @cache_wrapper(prefix="user_attempted")
def user_attempted_ids(profile): def user_attempted_ids(profile):
result = { result = {
id: { id: {
@ -248,3 +250,72 @@ def finished_submission(sub):
keys += ["contest_complete:%d" % participation.id] keys += ["contest_complete:%d" % participation.id]
keys += ["contest_attempted:%d" % participation.id] keys += ["contest_attempted:%d" % participation.id]
cache.delete_many(keys) cache.delete_many(keys)
class RecommendationType(Enum):
HOT_PROBLEM = 1
CF_DOT = 2
CF_COSINE = 3
CF_TIME_DOT = 4
CF_TIME_COSINE = 5
# Return a list of list. Each inner list correspond to each type in types
def get_user_recommended_problems(
user_id,
problem_ids,
recommendation_types,
limits,
shuffle=False,
):
cf_model = CollabFilter("collab_filter")
cf_time_model = CollabFilter("collab_filter_time")
def get_problem_ids_from_type(rec_type, limit):
if type(rec_type) == int:
try:
rec_type = RecommendationType(rec_type)
except ValueError:
raise Http404()
if rec_type == RecommendationType.HOT_PROBLEM:
return [
problem.id
for problem in hot_problems(timedelta(days=7), limit)
if problem.id in set(problem_ids)
]
if rec_type == RecommendationType.CF_DOT:
return cf_model.user_recommendations(
user_id, problem_ids, cf_model.DOT, limit
)
if rec_type == RecommendationType.CF_COSINE:
return cf_model.user_recommendations(
user_id, problem_ids, cf_model.COSINE, limit
)
if rec_type == RecommendationType.CF_TIME_DOT:
return cf_time_model.user_recommendations(
user_id, problem_ids, cf_model.DOT, limit
)
if rec_type == RecommendationType.CF_TIME_COSINE:
return cf_time_model.user_recommendations(
user_id, problem_ids, cf_model.COSINE, limit
)
return []
all_problems = []
for rec_type, limit in zip(recommendation_types, limits):
all_problems += get_problem_ids_from_type(rec_type, limit)
if shuffle:
seed = datetime.now().strftime("%d%m%Y")
random.Random(seed).shuffle(all_problems)
# deduplicate problems
res = []
used_pid = set()
for obj in all_problems:
if type(obj) == tuple:
obj = obj[1]
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res

View file

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

View file

@ -27,6 +27,8 @@ from django.db.models import (
Value, Value,
When, 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.db.models.expressions import CombinedExpression
from django.http import ( from django.http import (
Http404, Http404,
@ -67,6 +69,7 @@ from judge.models import (
Profile, Profile,
Submission, Submission,
ContestProblemClarification, ContestProblemClarification,
ContestsSummary,
) )
from judge.tasks import run_moss from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status from judge.utils.celery import redirect_to_task_status
@ -183,9 +186,16 @@ class ContestList(
self.request.GET.getlist("contest") self.request.GET.getlist("contest")
).strip() ).strip()
if query: if query:
queryset = queryset.filter( substr_queryset = queryset.filter(
Q(key__icontains=query) | Q(name__icontains=query) 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: if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id] self.org_query = [self.request.organization.id]
if self.show_orgs: if self.show_orgs:
@ -226,9 +236,10 @@ class ContestList(
active.append(participation) active.append(participation)
present.remove(participation.contest) present.remove(participation.contest)
active.sort(key=attrgetter("end_time", "key")) if not ("contest" in self.request.GET and settings.ENABLE_FTS):
present.sort(key=attrgetter("end_time", "key")) active.sort(key=attrgetter("end_time", "key"))
future.sort(key=attrgetter("start_time")) present.sort(key=attrgetter("end_time", "key"))
future.sort(key=attrgetter("start_time"))
context["active_participations"] = active context["active_participations"] = active
context["current_contests"] = present context["current_contests"] = present
context["future_contests"] = future context["future_contests"] = future
@ -408,7 +419,14 @@ class ContestDetail(
return [] return []
res = [] res = []
for organization in self.object.organizations.all(): for organization in self.object.organizations.all():
can_edit = False
if self.request.profile.can_edit_organization(organization): if self.request.profile.can_edit_organization(organization):
can_edit = True
if self.request.profile in organization and self.object.is_editable_by(
self.request.user
):
can_edit = True
if can_edit:
res.append(organization) res.append(organization)
return res return res
@ -430,16 +448,32 @@ class ContestDetail(
.add_i18n_name(self.request.LANGUAGE_CODE) .add_i18n_name(self.request.LANGUAGE_CODE)
) )
context["editable_organizations"] = self.get_editable_organizations() context["editable_organizations"] = self.get_editable_organizations()
context["is_clonable"] = is_contest_clonable(self.request, self.object)
return context return context
class ContestClone( def is_contest_clonable(request, contest):
ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView if not request.profile:
): return False
if not Organization.objects.filter(admins=request.profile).exists():
return False
if request.user.has_perm("judge.clone_contest"):
return True
if contest.ended:
return True
return False
class ContestClone(ContestMixin, TitleMixin, SingleObjectFormView):
title = _("Clone Contest") title = _("Clone Contest")
template_name = "contest/clone.html" template_name = "contest/clone.html"
form_class = ContestCloneForm form_class = ContestCloneForm
permission_required = "judge.clone_contest"
def get_object(self, queryset=None):
contest = super().get_object(queryset)
if not is_contest_clonable(self.request, contest):
raise Http404()
return contest
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
@ -464,6 +498,7 @@ class ContestClone(
contest.is_visible = False contest.is_visible = False
contest.user_count = 0 contest.user_count = 0
contest.key = form.cleaned_data["key"] contest.key = form.cleaned_data["key"]
contest.is_rated = False
contest.save() contest.save()
contest.tags.set(tags) contest.tags.set(tags)
@ -1380,3 +1415,65 @@ def update_contest_mode(request):
old_mode = request.session.get("contest_mode", True) old_mode = request.session.get("contest_mode", True)
request.session["contest_mode"] = not old_mode request.session["contest_mode"] = not old_mode
return HttpResponse() 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.db.models import Count, Q
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.urls import reverse from django.urls import reverse
from django.shortcuts import render
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
from judge.models import VolunteerProblemVote, Problem from judge.models import VolunteerProblemVote, Problem
@ -21,7 +22,7 @@ class InternalView(object):
class InternalProblem(InternalView, ListView): class InternalProblem(InternalView, ListView):
model = Problem model = Problem
title = _("Internal problems") title = _("Internal problems")
template_name = "internal/problem.html" template_name = "internal/problem/problem.html"
paginate_by = 100 paginate_by = 100
context_object_name = "problems" context_object_name = "problems"
@ -63,6 +64,28 @@ class InternalProblem(InternalView, ListView):
return context return context
def get_problem_votes(request):
if not request.user.is_superuser:
return HttpResponseForbidden()
try:
problem = Problem.objects.get(id=request.GET.get("id"))
except:
return HttpResponseForbidden()
votes = (
problem.volunteer_user_votes.select_related("voter")
.prefetch_related("types")
.order_by("id")
)
return render(
request,
"internal/problem/votes.html",
{
"problem": problem,
"votes": votes,
},
)
class RequestTimeMixin(object): class RequestTimeMixin(object):
def get_requests_data(self): def get_requests_data(self):
logger = logging.getLogger(self.log_name) logger = logging.getLogger(self.log_name)

View file

@ -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.views.generic import ListView
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.timezone import now from django.utils.timezone import now
from django.db.models import BooleanField, Value
from judge.utils.cachedict import CacheDict from judge.models import Profile, Notification, NotificationProfile
from judge.models import Profile, Comment, Notification from judge.models.notification import unseen_notifications_count
__all__ = ["NotificationList"] __all__ = ["NotificationList"]
@ -16,24 +15,11 @@ class NotificationList(ListView):
template_name = "notification/list.html" template_name = "notification/list.html"
def get_queryset(self): def get_queryset(self):
self.unseen_cnt = self.request.profile.count_unseen_notifications self.unseen_cnt = unseen_notifications_count(self.request.profile)
query = { self.queryset = Notification.objects.filter(
"owner": self.request.profile, owner=self.request.profile
} ).order_by("-id")[:100]
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
return self.queryset return self.queryset
@ -46,8 +32,6 @@ class NotificationList(ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
ret = super().get(request, *args, **kwargs) ret = super().get(request, *args, **kwargs)
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
# update after rendering unseen_notifications_count.dirty(self.request.profile)
Notification.objects.filter(owner=self.request.profile).update(read=True)
return ret return ret

View file

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

View file

@ -80,6 +80,7 @@ def vote_page(request, delta):
else: else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
break break
_dirty_vote_score(pagevote_id, request.profile)
return HttpResponse("success", content_type="text/plain") 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 = super(PageVoteDetailView, self).get_context_data(**kwargs)
context["pagevote"] = self.object.get_or_create_pagevote() context["pagevote"] = self.object.get_or_create_pagevote()
return context 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 logging
import os import os
import shutil import shutil
from datetime import timedelta, datetime
from operator import itemgetter from operator import itemgetter
from random import randrange from random import randrange
import random
from copy import deepcopy from copy import deepcopy
from django.core.cache import cache from django.core.cache import cache
@ -77,6 +75,8 @@ from judge.utils.problems import (
user_attempted_ids, user_attempted_ids,
user_completed_ids, user_completed_ids,
get_related_problems, get_related_problems,
get_user_recommended_problems,
RecommendationType,
) )
from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter from judge.utils.tickets import own_ticket_filter
@ -466,10 +466,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
manual_sort = frozenset(("name", "group", "solved", "type")) manual_sort = frozenset(("name", "group", "solved", "type"))
all_sorts = sql_sort | manual_sort all_sorts = sql_sort | manual_sort
default_desc = frozenset(("date", "points", "ac_rate", "user_count")) default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
default_sort = "-date"
first_page_href = None first_page_href = None
filter_organization = False 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( def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs 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: if not self.in_contest:
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
sort_key = self.order.lstrip("-") queryset = self.sort_queryset(queryset)
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("-"),
)
paginator.object_list = queryset paginator.object_list = queryset
return paginator 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 @cached_property
def profile(self): def profile(self):
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
@ -611,36 +619,28 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
self.request.GET.getlist("search") self.request.GET.getlist("search")
).strip() ).strip()
if query: if query:
if settings.ENABLE_FTS and self.full_text: substr_queryset = queryset.filter(
queryset = queryset.search(query, queryset.BOOLEAN).extra( Q(code__icontains=query)
order_by=["-relevance"] | 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: else:
queryset = queryset.filter( queryset = substr_queryset
Q(code__icontains=query)
| Q(name__icontains=query)
| Q(
translations__name__icontains=query,
translations__language=self.request.LANGUAGE_CODE,
)
)
self.prepoint_queryset = queryset self.prepoint_queryset = queryset
if self.point_start is not None: if self.point_start is not None:
queryset = queryset.filter(points__gte=self.point_start) queryset = queryset.filter(points__gte=self.point_start)
if self.point_end is not None: if self.point_end is not None:
queryset = queryset.filter(points__lte=self.point_end) 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() return queryset.distinct()
def get_queryset(self): 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["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["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["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"] = ( context["show_solved_only"] = (
0 if self.in_contest else int(self.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.show_types = self.GET_with_session(request, "show_types")
self.full_text = self.GET_with_session(request, "full_text") self.full_text = self.GET_with_session(request, "full_text")
self.show_editorial = self.GET_with_session(request, "show_editorial") 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.show_solved_only = self.GET_with_session(request, "show_solved_only")
self.search_query = None self.search_query = None
@ -816,7 +814,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
"show_types", "show_types",
"full_text", "full_text",
"show_editorial", "show_editorial",
"have_editorial",
"show_solved_only", "show_solved_only",
) )
for key in to_update: for key in to_update:
@ -837,24 +834,34 @@ class ProblemFeed(ProblemList, FeedView):
title = _("Problem feed") title = _("Problem feed")
feed_type = None feed_type = None
# arr = [[], [], ..] def get_recommended_problem_ids(self, queryset):
def merge_recommendation(self, arr): user_id = self.request.profile.id
seed = datetime.now().strftime("%d%m%Y") problem_ids = queryset.values_list("id", flat=True)
merged_array = [] rec_types = [
for a in arr: RecommendationType.CF_DOT,
merged_array += a RecommendationType.CF_COSINE,
random.Random(seed).shuffle(merged_array) RecommendationType.CF_TIME_DOT,
RecommendationType.CF_TIME_COSINE,
RecommendationType.HOT_PROBLEM,
]
limits = [100, 100, 100, 100, 20]
shuffle = True
res = [] allow_debug_type = (
used_pid = set() self.request.user.is_impersonate or self.request.user.is_superuser
)
if allow_debug_type and "debug_type" in self.request.GET:
try:
debug_type = int(self.request.GET.get("debug_type"))
except ValueError:
raise Http404()
rec_types = [debug_type]
limits = [100]
shuffle = False
for obj in merged_array: return get_user_recommended_problems(
if type(obj) == tuple: user_id, problem_ids, rec_types, limits, shuffle
obj = obj[1] )
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res
def get_queryset(self): def get_queryset(self):
if self.feed_type == "volunteer": if self.feed_type == "volunteer":
@ -862,9 +869,6 @@ class ProblemFeed(ProblemList, FeedView):
self.show_types = 1 self.show_types = 1
queryset = super(ProblemFeed, self).get_queryset() queryset = super(ProblemFeed, self).get_queryset()
if self.have_editorial:
queryset = queryset.filter(has_public_editorial=1)
user = self.request.profile user = self.request.profile
if self.feed_type == "new": if self.feed_type == "new":
@ -886,43 +890,13 @@ class ProblemFeed(ProblemList, FeedView):
.order_by("?") .order_by("?")
.add_i18n_name(self.request.LANGUAGE_CODE) .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: if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE) return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
cf_model = CollabFilter("collab_filter") q = self.get_recommended_problem_ids(queryset)
cf_time_model = CollabFilter("collab_filter_time")
queryset = queryset.values_list("id", flat=True)
hot_problems_recommendations = [
problem.id
for problem in hot_problems(timedelta(days=7), 20)
if problem.id in set(queryset)
]
q = self.merge_recommendation(
[
cf_model.user_recommendations(user, queryset, cf_model.DOT, 100),
cf_model.user_recommendations(
user,
queryset,
cf_model.COSINE,
100,
),
cf_time_model.user_recommendations(
user,
queryset,
cf_time_model.COSINE,
100,
),
cf_time_model.user_recommendations(
user,
queryset,
cf_time_model.DOT,
100,
),
hot_problems_recommendations,
]
)
queryset = Problem.objects.filter(id__in=q) queryset = Problem.objects.filter(id__in=q)
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
@ -946,7 +920,6 @@ class ProblemFeed(ProblemList, FeedView):
context["title"] = self.title context["title"] = self.title
context["feed_type"] = self.feed_type context["feed_type"] = self.feed_type
context["has_show_editorial_option"] = False context["has_show_editorial_option"] = False
context["has_have_editorial_option"] = False
return context return context

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import json import json
import os.path import os.path
import zipfile
from operator import attrgetter from operator import attrgetter
from django.conf import settings from django.conf import settings
@ -84,31 +83,7 @@ class SubmissionMixin(object):
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView): class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
submission = super(SubmissionDetailBase, self).get_object(queryset) submission = super(SubmissionDetailBase, self).get_object(queryset)
profile = self.request.profile if submission.is_accessible_by(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
):
return submission return submission
raise PermissionDenied() raise PermissionDenied()
@ -220,8 +195,8 @@ def get_cases_data(submission):
continue continue
count += 1 count += 1
problem_data[count] = { problem_data[count] = {
"input": case_data[case.input_file] if case.input_file else "", "input": case_data.get(case.input_file, "") if case.input_file else "",
"answer": case_data[case.output_file] if case.output_file else "", "answer": case_data.get(case.output_file, "") if case.output_file else "",
} }
return problem_data return problem_data
@ -483,19 +458,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
authenticated = self.request.user.is_authenticated authenticated = self.request.user.is_authenticated
context["dynamic_update"] = False context["dynamic_update"] = False
context["show_problem"] = self.show_problem context["show_problem"] = self.show_problem
context["completed_problem_ids"] = ( context["profile"] = self.request.profile
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["all_languages"] = Language.objects.all().values_list("key", "name") context["all_languages"] = Language.objects.all().values_list("key", "name")
context["selected_languages"] = self.selected_languages context["selected_languages"] = self.selected_languages
context["all_statuses"] = self.get_searchable_status_codes() context["all_statuses"] = self.get_searchable_status_codes()
context["selected_statuses"] = self.selected_statuses context["selected_statuses"] = self.selected_statuses
@ -779,19 +744,10 @@ def single_submission(request, submission_id, show_problem=True):
"submission/row.html", "submission/row.html",
{ {
"submission": submission, "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, "show_problem": show_problem,
"problem_name": show_problem "problem_name": show_problem
and submission.problem.translated_name(request.LANGUAGE_CODE), 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["contest"] = self.contest
context["problem"] = self.problem context["problem"] = self.problem
context["profile"] = self.profile 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) contest_problem = self.contest.contest_problems.get(problem=self.problem)
filtered_submissions = [] 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.utils.views import SingleObjectFormView, TitleMixin, paginate_query_context
from judge.views.problem import ProblemMixin from judge.views.problem import ProblemMixin
from judge.widgets import HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.models.notification import make_notification
ticket_widget = ( ticket_widget = (
forms.Textarea() forms.Textarea()
@ -49,16 +50,10 @@ ticket_widget = (
def add_ticket_notifications(users, author, link, ticket): def add_ticket_notifications(users, author, link, ticket):
html = f'<a href="{link}">{ticket.linked_item}</a>' html = f'<a href="{link}">{ticket.linked_item}</a>'
users = set(users) users = set(users)
if author in users: if author in users:
users.remove(author) users.remove(author)
make_notification(users, "Ticket", html, author)
for user in users:
notification = Notification(
owner=user, html_link=html, category="Ticket", author=author
)
notification.save()
class TicketForm(forms.Form): class TicketForm(forms.Form):

View file

@ -52,11 +52,11 @@ else:
class Media: class Media:
css = { css = {
"all": [ "all": [
"markdown.css",
"pagedown_widget.css", "pagedown_widget.css",
"content-description.css", "content-description.css",
"admin/css/pagedown.css", "admin/css/pagedown.css",
"pagedown.css", "pagedown.css",
"https://fonts.googleapis.com/css2?family=Fira+Code&family=Noto+Sans&display=swap",
] ]
} }
js = ["admin/js/pagedown.js"] 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" msgid "z-function"
msgstr "" 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; padding-right: 15px !important;
} }
.wmd-preview { .wmd-preview {
margin-top: 15px; margin-top: 15px;
padding: 15px; padding: 15px;
word-wrap: break-word; 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; border-color: #8a8a8a;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
} }
.blog-description { .blog-description {
@ -229,7 +230,6 @@
.show-more { .show-more {
display: flex; display: flex;
color: black; color: black;
font-style: italic;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
padding: 0px 12px; padding: 0px 12px;

View file

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

View file

@ -387,7 +387,7 @@ function onWindowReady() {
}); });
$('a').click(function() { $('a').click(function() {
var href = $(this).attr('href'); var href = $(this).attr('href');
if (href === '#' || href.startsWith("javascript")) { if (!href || href === '#' || href.startsWith("javascript")) {
return; 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); background-color: rgb(24, 26, 27);
box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 5px; box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 5px;
} }
.blog-box:hover { .blog-box:hover,
.blog-box:not(.pre-expand-blog) {
border-color: rgb(81, 88, 91); border-color: rgb(81, 88, 91);
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 2px;
} }
.problem-feed-name a { .problem-feed-name a {
color: rgb(102, 177, 250); color: rgb(102, 177, 250);
@ -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; box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset;
} }
textarea { textarea {
color: rgb(178, 172, 162);
background-image: none; background-image: none;
background-color: rgb(24, 26, 27); background-color: rgb(24, 26, 27);
border-color: rgb(62, 68, 70); border-color: rgb(62, 68, 70);
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset; box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset;
} }
textarea:hover { textarea:hover {
border-color: rgba(16, 87, 144, 0.8); border-color: rgb(140, 130, 115);
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset,
rgba(16, 91, 150, 0.6) 0px 0px 4px;
} }
input[type="text"]:hover, input[type="password"]:hover { input[type="text"]:hover, input[type="password"]:hover {
border-color: rgba(16, 87, 144, 0.8); 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; rgba(16, 91, 150, 0.6) 0px 0px 4px;
} }
textarea:focus { textarea:focus {
border-color: rgba(16, 87, 144, 0.8); border-color: rgb(140, 130, 115); outline-color: initial;
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset,
rgba(16, 91, 150, 0.6) 0px 0px 8px; outline-color: initial;
} }
input[type="text"]:focus, input[type="password"]:focus { input[type="text"]:focus, input[type="password"]:focus {
border-color: rgba(16, 87, 144, 0.8); border-color: rgba(16, 87, 144, 0.8);
@ -2552,7 +2549,7 @@ input[type="text"]:focus, input[type="password"]:focus {
ul.pagination a:hover { ul.pagination a:hover {
color: rgb(232, 230, 227); color: rgb(232, 230, 227);
background-image: initial; background-image: initial;
background-color: rgb(8, 128, 104); background-color: rgb(163, 62, 18);
border-color: initial; border-color: initial;
} }
ul.pagination > li > a, ul.pagination > li > a,
@ -2563,14 +2560,14 @@ ul.pagination > li > span {
border-color: rgb(199, 70, 8); border-color: rgb(199, 70, 8);
} }
ul.pagination > .disabled-page > a { ul.pagination > .disabled-page > a {
color: rgb(157, 148, 136); color: rgb(223, 220, 215);
background-color: rgba(3, 66, 54, 0.5); background-color: rgb(137, 78, 57);
border-color: rgba(126, 117, 103, 0.5); border-color: rgb(199, 68, 21);
} }
ul.pagination > .disabled-page > span { ul.pagination > .disabled-page > span {
color: rgb(157, 148, 136); color: rgb(223, 220, 215);
background-color: rgba(3, 66, 54, 0.5); background-color: rgb(137, 78, 57);
border-color: rgba(126, 117, 103, 0.5); border-color: rgb(199, 68, 21);
} }
ul.pagination > .active-page > a { ul.pagination > .active-page > a {
color: rgb(232, 230, 227); color: rgb(232, 230, 227);
@ -2785,11 +2782,15 @@ a.voted {
border-left-color: rgb(48, 52, 54); border-left-color: rgb(48, 52, 54);
} }
.actionbar .actionbar-button { .actionbar .actionbar-button {
border-color: rgb(60, 65, 68); background-image: initial;
background-color: rgb(49, 53, 55);
}
.actionbar .actionbar-button a:hover {
color: inherit;
} }
.actionbar .actionbar-button:hover { .actionbar .actionbar-button:hover {
background-image: initial; background-image: initial;
background-color: rgb(49, 53, 55); background-color: rgb(73, 79, 82);
} }
.actionbar .dislike-button { .actionbar .dislike-button {
border-left-color: initial; border-left-color: initial;

View file

@ -13,7 +13,7 @@ div.dmmd-preview-update {
} }
div.dmmd-preview-content { div.dmmd-preview-content {
padding: 0 7px; padding: 0 8px;
} }
div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update { 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 { 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 { 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 { div.dmmd-no-button div.dmmd-preview-update {
display: none; display: none;
} }
div.dmmd-no-button div.dmmd-preview-content { div.dmmd-no-button div.dmmd-preview-content {
padding-bottom: 0; padding-bottom: 0;
} }
div.dmmd-no-button:not(.dmmd-preview-has-content) { div.dmmd-no-button:not(.dmmd-preview-has-content) {
display: none; display: none;
} }
div.dmmd-preview-stale { div.dmmd-preview-stale {
background: repeating-linear-gradient(-45deg, #fff, #fff 10px, #f8f8f8 10px, #f8f8f8 20px); background: repeating-linear-gradient(-45deg, #fff, #fff 10px, #f8f8f8 10px, #f8f8f8 20px);
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@
width: 100%; width: 100%;
background: #fff; background: #fff;
border: 1px solid DarkGray; 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 { .wmd-preview {

View file

@ -14,7 +14,7 @@
width: 100%; width: 100%;
background: #fff; background: #fff;
border: 1px solid DarkGray; 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 { .wmd-preview {
@ -174,51 +174,51 @@
/* Extra styles to allow for image upload */ /* Extra styles to allow for image upload */
.pagedown-image-upload { .pagedown-image-upload {
display: none; display: none;
z-index: 10001; z-index: 10001;
position: fixed; position: fixed;
background: white; background: white;
top: 50%; top: 50%;
left: 50%; left: 50%;
padding: 10px; padding: 10px;
width: 400px; width: 400px;
max-width: 90%; max-width: 90%;
transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0);
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5); box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
} }
.pagedown-image-upload .submit-row { .pagedown-image-upload .submit-row {
margin: 10px 0 0 0; margin: 10px 0 0 0;
} }
.pagedown-image-upload.show { .pagedown-image-upload.show {
display: block; display: block;
} }
.pagedown-image-upload .submit-loading { .pagedown-image-upload .submit-loading {
display: none; display: none;
vertical-align: middle; vertical-align: middle;
border: 4px solid #f3f3f3; /* Light grey */ border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #79aec8; /* Blue */ border-top: 4px solid #79aec8; /* Blue */
border-radius: 50%; border-radius: 50%;
width: 24px; width: 24px;
height: 24px; height: 24px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.pagedown-image-upload .submit-loading.show { .pagedown-image-upload .submit-loading.show {
display: inline-block; display: inline-block;
} }
.pagedown-image-upload .submit-input { .pagedown-image-upload .submit-input {
display: none; display: none;
} }
.pagedown-image-upload .submit-input.show { .pagedown-image-upload .submit-input.show {
display: inline-block; display: inline-block;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@
{% endif %} {% endif %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(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) { window.reply_comment = function (parent) {
var $comment_reply = $('#comment-' + parent + '-reply'); var $comment_reply = $('#comment-' + parent + '-reply');
var reply_id = 'reply-' + parent; var reply_id = 'reply-' + parent;
@ -145,6 +145,8 @@
$comment_loading.hide(); $comment_loading.hide();
var $comment = $("#comment-" + id + "-children"); var $comment = $("#comment-" + id + "-children");
$comment.append(data); $comment.append(data);
MathJax.typeset($('#comments')[0]);
register_time($('.time-with-rel'));
} }
}) })
} }
@ -187,6 +189,7 @@
$comment.append(data); $comment.append(data);
} }
MathJax.typeset($('#comments')[0]); MathJax.typeset($('#comments')[0]);
register_time($('.time-with-rel'));
} }
}) })
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,12 +29,6 @@
height: 2em; height: 2em;
padding-top: 4px; padding-top: 4px;
} }
@media(min-width: 800px) {
#content {
width: 99%;
margin-left: 0;
}
}
@media(max-width: 799px) { @media(max-width: 799px) {
#content { #content {
width: 100%; width: 100%;
@ -114,7 +108,7 @@
$('#go').click(clean_submit); $('#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(); prep_form();
($('<form>').attr('action', window.location.pathname + '?' + form_serialize()) ($('<form>').attr('action', window.location.pathname + '?' + form_serialize())
.append($('<input>').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken') .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"> <div class="content-description">
<h4>{{_('Recommended problems')}}:</h4> <h4>{{_('Recommended problems')}}:</h4>
<ul style="list-style-type: none; margin: 0.3em;"> <ul style="list-style-type: none; margin: 0.3em;">

View file

@ -34,13 +34,6 @@
<label for="show_editorial">{{ _('Show editorial') }}</label> <label for="show_editorial">{{ _('Show editorial') }}</label>
</div> </div>
{% endif %} {% 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 %} {% if organizations %}
<div class="filter-form-group"> <div class="filter-form-group">
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label> <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