commit
9211bd1788
106 changed files with 3223 additions and 1794 deletions
|
@ -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
|
||||||
|
|
33
chat_box/migrations/0015_room_last_msg_time.py
Normal file
33
chat_box/migrations/0015_room_last_msg_time.py
Normal 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),
|
||||||
|
]
|
|
@ -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:
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
16
dmoj/urls.py
16
dmoj/urls.py
|
@ -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(),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
2
judge/bridge/utils.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class VanishedSubmission(Exception):
|
||||||
|
pass
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -21,7 +21,6 @@ from . import (
|
||||||
render,
|
render,
|
||||||
social,
|
social,
|
||||||
spaceless,
|
spaceless,
|
||||||
submission,
|
|
||||||
timedelta,
|
timedelta,
|
||||||
)
|
)
|
||||||
from . import registry
|
from . import registry
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
7
judge/logging.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
error_log = logging.getLogger("judge.errors")
|
||||||
|
|
||||||
|
|
||||||
|
def log_exception(msg):
|
||||||
|
error_log.exception(msg)
|
|
@ -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
30
judge/migrations/0170_contests_summary.py
Normal file
30
judge/migrations/0170_contests_summary.py
Normal 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")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
68
judge/migrations/0171_update_notification.py
Normal file
68
judge/migrations/0171_update_notification.py
Normal 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
judge/migrations/0172_index_rating.py
Normal file
18
judge/migrations/0172_index_rating.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
25
judge/migrations/0173_fulltext.py
Normal file
25
judge/migrations/0173_fulltext.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
61
judge/models/notification.py
Normal file
61
judge/models/notification.py
Normal 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
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
14
judge/views/markdown_editor.py
Normal file
14
judge/views/markdown_editor.py
Normal 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"),
|
||||||
|
},
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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": {},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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); }
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -113,7 +113,7 @@
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.writeText(link)
|
.writeText(link)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showTooltip(element, "Copied link", 'n');
|
showTooltip(element, "{{_('Copied link')}}", 'n');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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%');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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" %}
|
|
|
@ -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) }}"
|
||||||
|
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
71
templates/contest/contests_summary.html
Normal file
71
templates/contest/contests_summary.html
Normal 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 %}
|
|
@ -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) %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
29
templates/internal/problem/votes.html
Normal file
29
templates/internal/problem/votes.html
Normal 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>
|
143
templates/markdown_editor/markdown_editor.html
Normal file
143
templates/markdown_editor/markdown_editor.html
Normal 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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
<!--
|
|
||||||
-->
|
|
|
@ -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')
|
||||||
|
|
|
@ -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;">
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue