Merge branch 'LQDJudge:master' into master
This commit is contained in:
commit
c414ba62ed
59 changed files with 840 additions and 458 deletions
|
@ -18,6 +18,9 @@ class Room(models.Model):
|
|||
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
app_label = "chat_box"
|
||||
|
||||
@cache_wrapper(prefix="Rc")
|
||||
def contain(self, profile):
|
||||
return self.user_one == profile or self.user_two == profile
|
||||
|
@ -58,6 +61,7 @@ class Message(models.Model):
|
|||
indexes = [
|
||||
models.Index(fields=["hidden", "room", "-id"]),
|
||||
]
|
||||
app_label = "chat_box"
|
||||
|
||||
|
||||
class UserRoom(models.Model):
|
||||
|
@ -70,6 +74,7 @@ class UserRoom(models.Model):
|
|||
|
||||
class Meta:
|
||||
unique_together = ("user", "room")
|
||||
app_label = "chat_box"
|
||||
|
||||
|
||||
class Ignore(models.Model):
|
||||
|
@ -82,6 +87,9 @@ class Ignore(models.Model):
|
|||
)
|
||||
ignored_users = models.ManyToManyField(Profile)
|
||||
|
||||
class Meta:
|
||||
app_label = "chat_box"
|
||||
|
||||
@classmethod
|
||||
def is_ignored(self, current_user, new_friend):
|
||||
try:
|
||||
|
|
|
@ -8,6 +8,8 @@ from django.db.models.functions import Coalesce
|
|||
|
||||
from chat_box.models import Ignore, Message, UserRoom, Room
|
||||
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
secret_key = settings.CHAT_SECRET_KEY
|
||||
fernet = Fernet(secret_key)
|
||||
|
||||
|
@ -37,6 +39,7 @@ def encrypt_channel(channel):
|
|||
)
|
||||
|
||||
|
||||
@cache_wrapper(prefix="gub")
|
||||
def get_unread_boxes(profile):
|
||||
ignored_rooms = Ignore.get_ignored_rooms(profile)
|
||||
unread_boxes = (
|
||||
|
|
|
@ -36,7 +36,7 @@ from judge.jinja2.gravatar import gravatar
|
|||
from judge.models import Friend
|
||||
|
||||
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
|
||||
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel
|
||||
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
|
||||
|
||||
import json
|
||||
|
||||
|
@ -208,6 +208,7 @@ def post_message(request):
|
|||
)
|
||||
else:
|
||||
Room.last_message_body.dirty(room)
|
||||
|
||||
for user in room.users():
|
||||
event.post(
|
||||
encrypt_channel("chat_" + str(user.id)),
|
||||
|
@ -223,6 +224,7 @@ def post_message(request):
|
|||
UserRoom.objects.filter(user=user, room=room).update(
|
||||
unread_count=F("unread_count") + 1
|
||||
)
|
||||
get_unread_boxes.dirty(user)
|
||||
|
||||
return JsonResponse(ret)
|
||||
|
||||
|
@ -285,6 +287,8 @@ def update_last_seen(request, **kwargs):
|
|||
user_room.unread_count = 0
|
||||
user_room.save()
|
||||
|
||||
get_unread_boxes.dirty(profile)
|
||||
|
||||
return JsonResponse({"msg": "updated"})
|
||||
|
||||
|
||||
|
|
|
@ -517,6 +517,11 @@ urlpatterns = [
|
|||
),
|
||||
),
|
||||
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
||||
url(
|
||||
r"^contests/summary/(?P<key>\w+)$",
|
||||
contests.contests_summary_view,
|
||||
name="contests_summary",
|
||||
),
|
||||
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
||||
url(
|
||||
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
||||
|
|
|
@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry
|
|||
from django.contrib.auth.models import User
|
||||
|
||||
from judge.admin.comments import CommentAdmin
|
||||
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
|
||||
from judge.admin.contest import (
|
||||
ContestAdmin,
|
||||
ContestParticipationAdmin,
|
||||
ContestTagAdmin,
|
||||
ContestsSummaryAdmin,
|
||||
)
|
||||
from judge.admin.interface import (
|
||||
BlogPostAdmin,
|
||||
LicenseAdmin,
|
||||
|
@ -41,6 +46,7 @@ from judge.models import (
|
|||
Ticket,
|
||||
VolunteerProblemVote,
|
||||
Course,
|
||||
ContestsSummary,
|
||||
)
|
||||
|
||||
|
||||
|
@ -69,3 +75,4 @@ admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
|||
admin.site.register(Course)
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
|
||||
|
|
|
@ -502,3 +502,19 @@ class ContestParticipationAdmin(admin.ModelAdmin):
|
|||
|
||||
show_virtual.short_description = _("virtual")
|
||||
show_virtual.admin_order_field = "virtual"
|
||||
|
||||
|
||||
class ContestsSummaryForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
"contests": AdminHeavySelect2MultipleWidget(
|
||||
data_view="contest_select2", attrs={"style": "width: 100%"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ContestsSummaryAdmin(admin.ModelAdmin):
|
||||
fields = ("key", "contests", "scores")
|
||||
list_display = ("key",)
|
||||
search_fields = ("key", "contests__key")
|
||||
form = ContestsSummaryForm
|
||||
|
|
|
@ -25,6 +25,7 @@ from judge.models import (
|
|||
Solution,
|
||||
Notification,
|
||||
)
|
||||
from judge.models.notification import make_notification
|
||||
from judge.widgets import (
|
||||
AdminHeavySelect2MultipleWidget,
|
||||
AdminSelect2MultipleWidget,
|
||||
|
@ -32,6 +33,7 @@ from judge.widgets import (
|
|||
CheckboxSelectMultipleWithSelectAll,
|
||||
HeavyPreviewAdminPageDownWidget,
|
||||
)
|
||||
from judge.utils.problems import user_editable_ids, user_tester_ids
|
||||
|
||||
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
|
||||
|
||||
|
@ -358,12 +360,31 @@ class ProblemAdmin(CompareVersionAdmin):
|
|||
self._rescore(request, obj.id)
|
||||
|
||||
def save_related(self, request, form, formsets, change):
|
||||
editors = set()
|
||||
testers = set()
|
||||
if "curators" in form.changed_data or "authors" in form.changed_data:
|
||||
editors = set(form.instance.editor_ids)
|
||||
if "testers" in form.changed_data:
|
||||
testers = set(form.instance.tester_ids)
|
||||
|
||||
super().save_related(request, form, formsets, change)
|
||||
# Only rescored if we did not already do so in `save_model`
|
||||
obj = form.instance
|
||||
obj.curators.add(request.profile)
|
||||
obj.is_organization_private = obj.organizations.count() > 0
|
||||
obj.save()
|
||||
|
||||
if "curators" in form.changed_data or "authors" in form.changed_data:
|
||||
del obj.editor_ids
|
||||
editors = editors.union(set(obj.editor_ids))
|
||||
if "testers" in form.changed_data:
|
||||
del obj.tester_ids
|
||||
testers = testers.union(set(obj.tester_ids))
|
||||
|
||||
for editor in editors:
|
||||
user_editable_ids.dirty(editor)
|
||||
for tester in testers:
|
||||
user_tester_ids.dirty(tester)
|
||||
|
||||
# Create notification
|
||||
if "is_public" in form.changed_data or "organizations" in form.changed_data:
|
||||
users = set(obj.authors.all())
|
||||
|
@ -381,14 +402,7 @@ class ProblemAdmin(CompareVersionAdmin):
|
|||
category = "Problem public: " + str(obj.is_public)
|
||||
if orgs:
|
||||
category += " (" + ", ".join(orgs) + ")"
|
||||
for user in users:
|
||||
notification = Notification(
|
||||
owner=user,
|
||||
html_link=html,
|
||||
category=category,
|
||||
author=request.profile,
|
||||
)
|
||||
notification.save()
|
||||
make_notification(users, category, html, request.profile)
|
||||
|
||||
def construct_change_message(self, request, form, *args, **kwargs):
|
||||
if form.cleaned_data.get("change_message"):
|
||||
|
|
|
@ -126,7 +126,7 @@ class ProfileAdmin(VersionAdmin):
|
|||
admin_user_admin.short_description = _("User")
|
||||
|
||||
def email(self, obj):
|
||||
return obj.user.email
|
||||
return obj.email
|
||||
|
||||
email.admin_order_field = "user__email"
|
||||
email.short_description = _("Email")
|
||||
|
|
|
@ -1,49 +1,75 @@
|
|||
from inspect import signature
|
||||
from django.core.cache import cache
|
||||
from django.core.cache import cache, caches
|
||||
from django.db.models.query import QuerySet
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
|
||||
import hashlib
|
||||
|
||||
MAX_NUM_CHAR = 15
|
||||
MAX_NUM_CHAR = 50
|
||||
NONE_RESULT = "__None__"
|
||||
|
||||
|
||||
def cache_wrapper(prefix, timeout=None):
|
||||
def arg_to_str(arg):
|
||||
if hasattr(arg, "id"):
|
||||
return str(arg.id)
|
||||
if isinstance(arg, list) or isinstance(arg, QuerySet):
|
||||
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
|
||||
if len(str(arg)) > MAX_NUM_CHAR:
|
||||
return str(arg)[:MAX_NUM_CHAR]
|
||||
return str(arg)
|
||||
def arg_to_str(arg):
|
||||
if hasattr(arg, "id"):
|
||||
return str(arg.id)
|
||||
if isinstance(arg, list) or isinstance(arg, QuerySet):
|
||||
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
|
||||
if len(str(arg)) > MAX_NUM_CHAR:
|
||||
return str(arg)[:MAX_NUM_CHAR]
|
||||
return str(arg)
|
||||
|
||||
|
||||
def filter_args(args_list):
|
||||
return [x for x in args_list if not isinstance(x, WSGIRequest)]
|
||||
|
||||
|
||||
l0_cache = caches["l0"] if "l0" in caches else None
|
||||
|
||||
|
||||
def cache_wrapper(prefix, timeout=None):
|
||||
def get_key(func, *args, **kwargs):
|
||||
args_list = list(args)
|
||||
signature_args = list(signature(func).parameters.keys())
|
||||
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
|
||||
args_list = filter_args(args_list)
|
||||
args_list = [arg_to_str(i) for i in args_list]
|
||||
key = prefix + ":" + ":".join(args_list)
|
||||
key = key.replace(" ", "_")
|
||||
return key
|
||||
|
||||
def _get(key):
|
||||
if not l0_cache:
|
||||
return cache.get(key)
|
||||
return l0_cache.get(key) or cache.get(key)
|
||||
|
||||
def _set_l0(key, value):
|
||||
if l0_cache:
|
||||
l0_cache.set(key, value, 30)
|
||||
|
||||
def _set(key, value, timeout):
|
||||
_set_l0(key, value)
|
||||
cache.set(key, value, timeout)
|
||||
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
cache_key = get_key(func, *args, **kwargs)
|
||||
result = cache.get(cache_key)
|
||||
result = _get(cache_key)
|
||||
if result is not None:
|
||||
_set_l0(cache_key, result)
|
||||
if result == NONE_RESULT:
|
||||
result = None
|
||||
return result
|
||||
result = func(*args, **kwargs)
|
||||
if result is None:
|
||||
result = NONE_RESULT
|
||||
result = func(*args, **kwargs)
|
||||
cache.set(cache_key, result, timeout)
|
||||
_set(cache_key, result, timeout)
|
||||
return result
|
||||
|
||||
def dirty(*args, **kwargs):
|
||||
cache_key = get_key(func, *args, **kwargs)
|
||||
cache.delete(cache_key)
|
||||
if l0_cache:
|
||||
l0_cache.delete(cache_key)
|
||||
|
||||
wrapper.dirty = dirty
|
||||
|
||||
|
|
|
@ -26,21 +26,20 @@ from judge.dblock import LockModel
|
|||
from judge.models import Comment, Notification
|
||||
from judge.widgets import HeavyPreviewPageDownWidget
|
||||
from judge.jinja2.reference import get_user_from_text
|
||||
from judge.models.notification import make_notification
|
||||
|
||||
|
||||
DEFAULT_OFFSET = 10
|
||||
|
||||
|
||||
def _get_html_link_notification(comment):
|
||||
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
|
||||
|
||||
|
||||
def add_mention_notifications(comment):
|
||||
user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id)
|
||||
for user in user_referred:
|
||||
notification_ref = Notification(owner=user, comment=comment, category="Mention")
|
||||
notification_ref.save()
|
||||
|
||||
|
||||
def del_mention_notifications(comment):
|
||||
query = {"comment": comment, "category": "Mention"}
|
||||
Notification.objects.filter(**query).delete()
|
||||
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
|
||||
link = _get_html_link_notification(comment)
|
||||
make_notification(users_mentioned, "Mention", link, comment.author)
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
|
@ -124,23 +123,17 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
|||
comment.save()
|
||||
|
||||
# add notification for reply
|
||||
comment_notif_link = _get_html_link_notification(comment)
|
||||
if comment.parent and comment.parent.author != comment.author:
|
||||
notification_reply = Notification(
|
||||
owner=comment.parent.author, comment=comment, category="Reply"
|
||||
make_notification(
|
||||
[comment.parent.author], "Reply", comment_notif_link, comment.author
|
||||
)
|
||||
notification_reply.save()
|
||||
|
||||
# add notification for page authors
|
||||
page_authors = comment.linked_object.authors.all()
|
||||
for user in page_authors:
|
||||
if user == comment.author:
|
||||
continue
|
||||
notification = Notification(
|
||||
owner=user, comment=comment, category="Comment"
|
||||
)
|
||||
notification.save()
|
||||
# except Exception:
|
||||
# pass
|
||||
make_notification(
|
||||
page_authors, "Comment", comment_notif_link, comment.author
|
||||
)
|
||||
|
||||
add_mention_notifications(comment)
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ from . import (
|
|||
render,
|
||||
social,
|
||||
spaceless,
|
||||
submission,
|
||||
timedelta,
|
||||
)
|
||||
from . import registry
|
||||
|
|
|
@ -12,12 +12,12 @@ from . import registry
|
|||
def gravatar(profile, size=80, default=None, profile_image=None, email=None):
|
||||
if profile_image:
|
||||
return profile_image
|
||||
if profile and profile.profile_image:
|
||||
return profile.profile_image.url
|
||||
if profile and profile.profile_image_url:
|
||||
return profile.profile_image_url
|
||||
if profile:
|
||||
email = email or profile.user.email
|
||||
email = email or profile.email
|
||||
if default is None:
|
||||
default = profile.mute
|
||||
default = profile.is_muted
|
||||
gravatar_url = (
|
||||
"//www.gravatar.com/avatar/"
|
||||
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
|
||||
|
|
|
@ -157,14 +157,14 @@ def item_title(item):
|
|||
@registry.render_with("user/link.html")
|
||||
def link_user(user):
|
||||
if isinstance(user, Profile):
|
||||
user, profile = user.user, user
|
||||
profile = user
|
||||
elif isinstance(user, AbstractUser):
|
||||
profile = user.profile
|
||||
elif type(user).__name__ == "ContestRankingProfile":
|
||||
user, profile = user.user, user
|
||||
profile = user
|
||||
else:
|
||||
raise ValueError("Expected profile or user, got %s" % (type(user),))
|
||||
return {"user": user, "profile": profile}
|
||||
return {"profile": profile}
|
||||
|
||||
|
||||
@registry.function
|
||||
|
|
|
@ -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
|
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,
|
||||
),
|
||||
]
|
|
@ -6,7 +6,7 @@ from judge.models.choices import (
|
|||
MATH_ENGINES_CHOICES,
|
||||
TIMEZONE,
|
||||
)
|
||||
from judge.models.comment import Comment, CommentLock, CommentVote, Notification
|
||||
from judge.models.comment import Comment, CommentLock, CommentVote
|
||||
from judge.models.contest import (
|
||||
Contest,
|
||||
ContestMoss,
|
||||
|
@ -16,6 +16,7 @@ from judge.models.contest import (
|
|||
ContestTag,
|
||||
Rating,
|
||||
ContestProblemClarification,
|
||||
ContestsSummary,
|
||||
)
|
||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||
|
@ -57,6 +58,7 @@ from judge.models.volunteer import VolunteerProblemVote
|
|||
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||
from judge.models.bookmark import BookMark, MakeBookMark
|
||||
from judge.models.course import Course
|
||||
from judge.models.notification import Notification, NotificationProfile
|
||||
|
||||
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
||||
revisions.register(Problem, follow=["language_limits"])
|
||||
|
|
|
@ -177,29 +177,3 @@ class CommentLock(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return str(self.page)
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
owner = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("owner"),
|
||||
related_name="notifications",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
|
||||
comment = models.ForeignKey(
|
||||
Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE
|
||||
)
|
||||
read = models.BooleanField(verbose_name=_("read"), default=False)
|
||||
category = models.CharField(verbose_name=_("category"), max_length=1000)
|
||||
html_link = models.TextField(
|
||||
default="",
|
||||
verbose_name=_("html link to comments, used for non-comments"),
|
||||
max_length=1000,
|
||||
)
|
||||
author = models.ForeignKey(
|
||||
Profile,
|
||||
null=True,
|
||||
verbose_name=_("who trigger, used for non-comment"),
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
|
|
|
@ -24,6 +24,7 @@ from judge.models.submission import Submission
|
|||
from judge.ratings import rate_contest
|
||||
from judge.models.pagevote import PageVotable
|
||||
from judge.models.bookmark import Bookmarkable
|
||||
from judge.fulltext import SearchManager
|
||||
|
||||
__all__ = [
|
||||
"Contest",
|
||||
|
@ -33,6 +34,7 @@ __all__ = [
|
|||
"ContestSubmission",
|
||||
"Rating",
|
||||
"ContestProblemClarification",
|
||||
"ContestsSummary",
|
||||
]
|
||||
|
||||
|
||||
|
@ -308,6 +310,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
|||
comments = GenericRelation("Comment")
|
||||
pagevote = GenericRelation("PageVote")
|
||||
bookmark = GenericRelation("BookMark")
|
||||
objects = SearchManager(("key", "name"))
|
||||
|
||||
@cached_property
|
||||
def format_class(self):
|
||||
|
@ -900,3 +903,27 @@ class ContestProblemClarification(models.Model):
|
|||
date = models.DateTimeField(
|
||||
verbose_name=_("clarification timestamp"), auto_now_add=True
|
||||
)
|
||||
|
||||
|
||||
class ContestsSummary(models.Model):
|
||||
contests = models.ManyToManyField(
|
||||
Contest,
|
||||
)
|
||||
scores = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
key = models.CharField(
|
||||
max_length=20,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("contests summary")
|
||||
verbose_name_plural = _("contests summaries")
|
||||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("contests_summary", args=[self.key])
|
||||
|
|
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 judge.models.profile import Profile
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
__all__ = ["PageVote", "PageVoteVoter"]
|
||||
|
||||
|
@ -28,6 +29,7 @@ class PageVote(models.Model):
|
|||
]
|
||||
unique_together = ("content_type", "object_id")
|
||||
|
||||
@cache_wrapper(prefix="PVvs")
|
||||
def vote_score(self, user):
|
||||
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
|
||||
if page_vote.exists():
|
||||
|
|
|
@ -107,9 +107,7 @@ class License(models.Model):
|
|||
|
||||
class TranslatedProblemQuerySet(SearchQuerySet):
|
||||
def __init__(self, **kwargs):
|
||||
super(TranslatedProblemQuerySet, self).__init__(
|
||||
("code", "name", "description"), **kwargs
|
||||
)
|
||||
super(TranslatedProblemQuerySet, self).__init__(("code", "name"), **kwargs)
|
||||
|
||||
def add_i18n_name(self, language):
|
||||
return self.annotate(
|
||||
|
@ -436,15 +434,23 @@ class Problem(models.Model, PageVotable, Bookmarkable):
|
|||
|
||||
@cached_property
|
||||
def author_ids(self):
|
||||
return self.authors.values_list("id", flat=True)
|
||||
return Problem.authors.through.objects.filter(problem=self).values_list(
|
||||
"profile_id", flat=True
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def editor_ids(self):
|
||||
return self.author_ids | self.curators.values_list("id", flat=True)
|
||||
return self.author_ids.union(
|
||||
Problem.curators.through.objects.filter(problem=self).values_list(
|
||||
"profile_id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def tester_ids(self):
|
||||
return self.testers.values_list("id", flat=True)
|
||||
return Problem.testers.through.objects.filter(problem=self).values_list(
|
||||
"profile_id", flat=True
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def usable_common_names(self):
|
||||
|
|
|
@ -10,12 +10,17 @@ from django.urls import reverse
|
|||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
|
||||
|
||||
from fernet_fields import EncryptedCharField
|
||||
from sortedm2m.fields import SortedManyToManyField
|
||||
|
||||
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
|
||||
from judge.models.runtime import Language
|
||||
from judge.ratings import rating_class
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
|
||||
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
|
||||
|
@ -142,6 +147,7 @@ class Organization(models.Model):
|
|||
)
|
||||
verbose_name = _("organization")
|
||||
verbose_name_plural = _("organizations")
|
||||
app_label = "judge"
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
|
@ -200,7 +206,7 @@ class Profile(models.Model):
|
|||
help_text=_("User will not be able to vote on problems' point values."),
|
||||
default=False,
|
||||
)
|
||||
rating = models.IntegerField(null=True, default=None)
|
||||
rating = models.IntegerField(null=True, default=None, db_index=True)
|
||||
user_script = models.TextField(
|
||||
verbose_name=_("user script"),
|
||||
default="",
|
||||
|
@ -254,6 +260,24 @@ class Profile(models.Model):
|
|||
max_length=300,
|
||||
)
|
||||
|
||||
@cache_wrapper(prefix="Pgbi2")
|
||||
def _get_basic_info(self):
|
||||
profile_image_url = None
|
||||
if self.profile_image:
|
||||
profile_image_url = self.profile_image.url
|
||||
return {
|
||||
"first_name": self.user.first_name,
|
||||
"last_name": self.user.last_name,
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
"mute": self.mute,
|
||||
"profile_image_url": profile_image_url,
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def _cached_info(self):
|
||||
return self._get_basic_info()
|
||||
|
||||
@cached_property
|
||||
def organization(self):
|
||||
# We do this to take advantage of prefetch_related
|
||||
|
@ -262,14 +286,33 @@ class Profile(models.Model):
|
|||
|
||||
@cached_property
|
||||
def username(self):
|
||||
return self.user.username
|
||||
return self._cached_info["username"]
|
||||
|
||||
@cached_property
|
||||
def first_name(self):
|
||||
return self._cached_info["first_name"]
|
||||
|
||||
@cached_property
|
||||
def last_name(self):
|
||||
return self._cached_info["last_name"]
|
||||
|
||||
@cached_property
|
||||
def email(self):
|
||||
return self._cached_info["email"]
|
||||
|
||||
@cached_property
|
||||
def is_muted(self):
|
||||
return self._cached_info["mute"]
|
||||
|
||||
@cached_property
|
||||
def profile_image_url(self):
|
||||
return self._cached_info["profile_image_url"]
|
||||
|
||||
@cached_property
|
||||
def count_unseen_notifications(self):
|
||||
query = {
|
||||
"read": False,
|
||||
}
|
||||
return self.notifications.filter(**query).count()
|
||||
from judge.models.notification import unseen_notifications_count
|
||||
|
||||
return unseen_notifications_count(self)
|
||||
|
||||
@cached_property
|
||||
def count_unread_chat_boxes(self):
|
||||
|
@ -498,3 +541,21 @@ class OrganizationProfile(models.Model):
|
|||
@classmethod
|
||||
def get_most_recent_organizations(self, users):
|
||||
return self.objects.filter(users=users).order_by("-last_visit")[:5]
|
||||
|
||||
|
||||
@receiver([post_save], sender=User)
|
||||
def on_user_save(sender, instance, **kwargs):
|
||||
try:
|
||||
profile = instance.profile
|
||||
profile._get_basic_info.dirty(profile)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@receiver([pre_save], sender=Profile)
|
||||
def on_profile_save(sender, instance, **kwargs):
|
||||
if instance.id is None:
|
||||
return
|
||||
prev = sender.objects.get(id=instance.id)
|
||||
if prev.mute != instance.mute or prev.profile_image != instance.profile_image:
|
||||
instance._get_basic_info.dirty(instance)
|
||||
|
|
|
@ -220,6 +220,46 @@ class Submission(models.Model):
|
|||
def id_secret(self):
|
||||
return self.get_id_secret(self.id)
|
||||
|
||||
def is_accessible_by(self, profile):
|
||||
from judge.utils.problems import (
|
||||
user_completed_ids,
|
||||
user_tester_ids,
|
||||
user_editable_ids,
|
||||
)
|
||||
|
||||
if not profile:
|
||||
return False
|
||||
|
||||
problem_id = self.problem_id
|
||||
user = profile.user
|
||||
|
||||
if profile.id == self.user_id:
|
||||
return True
|
||||
|
||||
if problem_id in user_editable_ids(profile):
|
||||
return True
|
||||
|
||||
if self.problem_id in user_completed_ids(profile):
|
||||
if self.problem.is_public:
|
||||
return True
|
||||
if problem_id in user_tester_ids(profile):
|
||||
return True
|
||||
|
||||
if user.has_perm("judge.change_submission"):
|
||||
return True
|
||||
|
||||
if user.has_perm("judge.view_all_submission"):
|
||||
return True
|
||||
|
||||
if self.problem.is_public and user.has_perm("judge.view_public_submission"):
|
||||
return True
|
||||
|
||||
contest = self.contest_object
|
||||
if contest and contest.is_editable_by(user):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("abort_any_submission", "Abort any submission"),
|
||||
|
|
|
@ -24,40 +24,41 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
@cache_wrapper(prefix="user_tester")
|
||||
def user_tester_ids(profile):
|
||||
return set(
|
||||
Problem.testers.through.objects.filter(profile=profile).values_list(
|
||||
"problem_id", flat=True
|
||||
)
|
||||
Problem.testers.through.objects.filter(profile=profile)
|
||||
.values_list("problem_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
@cache_wrapper(prefix="user_editable")
|
||||
def user_editable_ids(profile):
|
||||
result = set(
|
||||
(
|
||||
Problem.objects.filter(authors=profile)
|
||||
| Problem.objects.filter(curators=profile)
|
||||
).values_list("id", flat=True)
|
||||
)
|
||||
.values_list("id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@cache_wrapper(prefix="contest_complete")
|
||||
def contest_completed_ids(participation):
|
||||
key = "contest_complete:%d" % participation.id
|
||||
result = cache.get(key)
|
||||
if result is None:
|
||||
result = set(
|
||||
participation.submissions.filter(
|
||||
submission__result="AC", points=F("problem__points")
|
||||
)
|
||||
.values_list("problem__problem__id", flat=True)
|
||||
.distinct()
|
||||
result = set(
|
||||
participation.submissions.filter(
|
||||
submission__result="AC", points=F("problem__points")
|
||||
)
|
||||
cache.set(key, result, 86400)
|
||||
.values_list("problem__problem__id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@cache_wrapper(prefix="user_complete", timeout=86400)
|
||||
@cache_wrapper(prefix="user_complete")
|
||||
def user_completed_ids(profile):
|
||||
result = set(
|
||||
Submission.objects.filter(
|
||||
|
@ -69,7 +70,7 @@ def user_completed_ids(profile):
|
|||
return result
|
||||
|
||||
|
||||
@cache_wrapper(prefix="contest_attempted", timeout=86400)
|
||||
@cache_wrapper(prefix="contest_attempted")
|
||||
def contest_attempted_ids(participation):
|
||||
result = {
|
||||
id: {"achieved_points": points, "max_points": max_points}
|
||||
|
@ -84,7 +85,7 @@ def contest_attempted_ids(participation):
|
|||
return result
|
||||
|
||||
|
||||
@cache_wrapper(prefix="user_attempted", timeout=86400)
|
||||
@cache_wrapper(prefix="user_attempted")
|
||||
def user_attempted_ids(profile):
|
||||
result = {
|
||||
id: {
|
||||
|
|
|
@ -27,7 +27,7 @@ from judge.dblock import LockModel
|
|||
from judge.models import Comment, CommentVote, Notification, BlogPost
|
||||
from judge.utils.views import TitleMixin
|
||||
from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget
|
||||
from judge.comments import add_mention_notifications, del_mention_notifications
|
||||
from judge.comments import add_mention_notifications
|
||||
|
||||
import json
|
||||
|
||||
|
@ -240,7 +240,6 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
|
|||
def form_valid(self, form):
|
||||
# update notifications
|
||||
comment = form.instance
|
||||
del_mention_notifications(comment)
|
||||
add_mention_notifications(comment)
|
||||
|
||||
with transaction.atomic(), revisions.create_revision():
|
||||
|
|
|
@ -27,6 +27,8 @@ from django.db.models import (
|
|||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.expressions import CombinedExpression
|
||||
from django.http import (
|
||||
Http404,
|
||||
|
@ -67,6 +69,7 @@ from judge.models import (
|
|||
Profile,
|
||||
Submission,
|
||||
ContestProblemClarification,
|
||||
ContestsSummary,
|
||||
)
|
||||
from judge.tasks import run_moss
|
||||
from judge.utils.celery import redirect_to_task_status
|
||||
|
@ -183,9 +186,16 @@ class ContestList(
|
|||
self.request.GET.getlist("contest")
|
||||
).strip()
|
||||
if query:
|
||||
queryset = queryset.filter(
|
||||
substr_queryset = queryset.filter(
|
||||
Q(key__icontains=query) | Q(name__icontains=query)
|
||||
)
|
||||
if settings.ENABLE_FTS:
|
||||
queryset = (
|
||||
queryset.search(query).extra(order_by=["-relevance"])
|
||||
| substr_queryset
|
||||
)
|
||||
else:
|
||||
queryset = substr_queryset
|
||||
if not self.org_query and self.request.organization:
|
||||
self.org_query = [self.request.organization.id]
|
||||
if self.show_orgs:
|
||||
|
@ -226,9 +236,10 @@ class ContestList(
|
|||
active.append(participation)
|
||||
present.remove(participation.contest)
|
||||
|
||||
active.sort(key=attrgetter("end_time", "key"))
|
||||
present.sort(key=attrgetter("end_time", "key"))
|
||||
future.sort(key=attrgetter("start_time"))
|
||||
if not ("contest" in self.request.GET and settings.ENABLE_FTS):
|
||||
active.sort(key=attrgetter("end_time", "key"))
|
||||
present.sort(key=attrgetter("end_time", "key"))
|
||||
future.sort(key=attrgetter("start_time"))
|
||||
context["active_participations"] = active
|
||||
context["current_contests"] = present
|
||||
context["future_contests"] = future
|
||||
|
@ -1380,3 +1391,65 @@ def update_contest_mode(request):
|
|||
old_mode = request.session.get("contest_mode", True)
|
||||
request.session["contest_mode"] = not old_mode
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
ContestsSummaryData = namedtuple(
|
||||
"ContestsSummaryData",
|
||||
"user points point_contests css_class",
|
||||
)
|
||||
|
||||
|
||||
def contests_summary_view(request, key):
|
||||
try:
|
||||
contests_summary = ContestsSummary.objects.get(key=key)
|
||||
except:
|
||||
raise Http404()
|
||||
|
||||
cache_key = "csv:" + key
|
||||
context = cache.get(cache_key)
|
||||
if context:
|
||||
return render(request, "contest/contests_summary.html", context)
|
||||
|
||||
scores_system = contests_summary.scores
|
||||
contests = contests_summary.contests.all()
|
||||
total_points = defaultdict(int)
|
||||
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
|
||||
user_css_class = {}
|
||||
|
||||
for i in range(len(contests)):
|
||||
contest = contests[i]
|
||||
users, problems = get_contest_ranking_list(request, contest)
|
||||
for rank, user in users:
|
||||
curr_score = 0
|
||||
if rank - 1 < len(scores_system):
|
||||
curr_score = scores_system[rank - 1]
|
||||
total_points[user.user] += curr_score
|
||||
result_per_contest[user.user][i] = (curr_score, rank)
|
||||
user_css_class[user.user] = user.css_class
|
||||
|
||||
sorted_total_points = [
|
||||
ContestsSummaryData(
|
||||
user=user,
|
||||
points=total_points[user],
|
||||
point_contests=result_per_contest[user],
|
||||
css_class=user_css_class[user],
|
||||
)
|
||||
for user in total_points
|
||||
]
|
||||
|
||||
sorted_total_points.sort(key=lambda x: x.points, reverse=True)
|
||||
total_rank = ranker(sorted_total_points)
|
||||
|
||||
context = {
|
||||
"total_rank": list(total_rank),
|
||||
"title": _("Contests Summary"),
|
||||
"contests": contests,
|
||||
}
|
||||
cache.set(cache_key, context)
|
||||
|
||||
return render(request, "contest/contests_summary.html", context)
|
||||
|
||||
|
||||
@receiver([post_save, post_delete], sender=ContestsSummary)
|
||||
def clear_cache(sender, instance, **kwargs):
|
||||
cache.delete("csv:" + instance.key)
|
||||
|
|
|
@ -2,10 +2,9 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.views.generic import ListView
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.timezone import now
|
||||
from django.db.models import BooleanField, Value
|
||||
|
||||
from judge.utils.cachedict import CacheDict
|
||||
from judge.models import Profile, Comment, Notification
|
||||
from judge.models import Profile, Notification, NotificationProfile
|
||||
from judge.models.notification import unseen_notifications_count
|
||||
|
||||
__all__ = ["NotificationList"]
|
||||
|
||||
|
@ -16,24 +15,11 @@ class NotificationList(ListView):
|
|||
template_name = "notification/list.html"
|
||||
|
||||
def get_queryset(self):
|
||||
self.unseen_cnt = self.request.profile.count_unseen_notifications
|
||||
self.unseen_cnt = unseen_notifications_count(self.request.profile)
|
||||
|
||||
query = {
|
||||
"owner": self.request.profile,
|
||||
}
|
||||
|
||||
self.queryset = (
|
||||
Notification.objects.filter(**query)
|
||||
.order_by("-time")[:100]
|
||||
.annotate(seen=Value(True, output_field=BooleanField()))
|
||||
)
|
||||
|
||||
# Mark the several first unseen
|
||||
for cnt, q in enumerate(self.queryset):
|
||||
if cnt < self.unseen_cnt:
|
||||
q.seen = False
|
||||
else:
|
||||
break
|
||||
self.queryset = Notification.objects.filter(
|
||||
owner=self.request.profile
|
||||
).order_by("-id")[:100]
|
||||
|
||||
return self.queryset
|
||||
|
||||
|
@ -46,8 +32,6 @@ class NotificationList(ListView):
|
|||
|
||||
def get(self, request, *args, **kwargs):
|
||||
ret = super().get(request, *args, **kwargs)
|
||||
|
||||
# update after rendering
|
||||
Notification.objects.filter(owner=self.request.profile).update(read=True)
|
||||
|
||||
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
|
||||
unseen_notifications_count.dirty(self.request.profile)
|
||||
return ret
|
||||
|
|
|
@ -56,10 +56,10 @@ from judge.models import (
|
|||
Problem,
|
||||
Profile,
|
||||
Contest,
|
||||
Notification,
|
||||
ContestProblem,
|
||||
OrganizationProfile,
|
||||
)
|
||||
from judge.models.notification import make_notification
|
||||
from judge import event_poster as event
|
||||
from judge.utils.ranker import ranker
|
||||
from judge.utils.views import (
|
||||
|
@ -1019,16 +1019,9 @@ class AddOrganizationBlog(
|
|||
html = (
|
||||
f'<a href="{link}">{self.object.title} - {self.organization.name}</a>'
|
||||
)
|
||||
for user in self.organization.admins.all():
|
||||
if user.id == self.request.profile.id:
|
||||
continue
|
||||
notification = Notification(
|
||||
owner=user,
|
||||
author=self.request.profile,
|
||||
category="Add blog",
|
||||
html_link=html,
|
||||
)
|
||||
notification.save()
|
||||
make_notification(
|
||||
self.organization.admins.all(), "Add blog", html, self.request.profile
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
|
@ -1104,17 +1097,8 @@ class EditOrganizationBlog(
|
|||
)
|
||||
html = f'<a href="{link}">{blog.title} - {self.organization.name}</a>'
|
||||
post_authors = blog.authors.all()
|
||||
posible_user = self.organization.admins.all() | post_authors
|
||||
for user in posible_user:
|
||||
if user.id == self.request.profile.id:
|
||||
continue
|
||||
notification = Notification(
|
||||
owner=user,
|
||||
author=self.request.profile,
|
||||
category=action,
|
||||
html_link=html,
|
||||
)
|
||||
notification.save()
|
||||
posible_users = self.organization.admins.all() | post_authors
|
||||
make_notification(posible_users, action, html, self.request.profile)
|
||||
|
||||
def form_valid(self, form):
|
||||
with transaction.atomic(), revisions.create_revision():
|
||||
|
|
|
@ -80,6 +80,7 @@ def vote_page(request, delta):
|
|||
else:
|
||||
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
|
||||
break
|
||||
_dirty_vote_score(pagevote_id, request.profile)
|
||||
return HttpResponse("success", content_type="text/plain")
|
||||
|
||||
|
||||
|
@ -103,3 +104,8 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
|||
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
|
||||
context["pagevote"] = self.object.get_or_create_pagevote()
|
||||
return context
|
||||
|
||||
|
||||
def _dirty_vote_score(pagevote_id, profile):
|
||||
pv = PageVote(id=pagevote_id)
|
||||
pv.vote_score.dirty(pv, profile)
|
||||
|
|
|
@ -466,10 +466,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
manual_sort = frozenset(("name", "group", "solved", "type"))
|
||||
all_sorts = sql_sort | manual_sort
|
||||
default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
|
||||
default_sort = "-date"
|
||||
first_page_href = None
|
||||
filter_organization = False
|
||||
|
||||
def get_default_sort_order(self, request):
|
||||
if "search" in request.GET and settings.ENABLE_FTS:
|
||||
return "-relevance"
|
||||
return "-date"
|
||||
|
||||
def get_paginator(
|
||||
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
|
||||
):
|
||||
|
@ -485,42 +489,46 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
)
|
||||
if not self.in_contest:
|
||||
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
|
||||
sort_key = self.order.lstrip("-")
|
||||
if sort_key in self.sql_sort:
|
||||
queryset = queryset.order_by(self.order)
|
||||
elif sort_key == "name":
|
||||
queryset = queryset.order_by(self.order.replace("name", "i18n_name"))
|
||||
elif sort_key == "group":
|
||||
queryset = queryset.order_by(self.order + "__name")
|
||||
elif sort_key == "solved":
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
solved = user_completed_ids(profile)
|
||||
attempted = user_attempted_ids(profile)
|
||||
|
||||
def _solved_sort_order(problem):
|
||||
if problem.id in solved:
|
||||
return 1
|
||||
if problem.id in attempted:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=_solved_sort_order, reverse=self.order.startswith("-")
|
||||
)
|
||||
elif sort_key == "type":
|
||||
if self.show_types:
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=lambda problem: problem.types_list[0]
|
||||
if problem.types_list
|
||||
else "",
|
||||
reverse=self.order.startswith("-"),
|
||||
)
|
||||
queryset = self.sort_queryset(queryset)
|
||||
paginator.object_list = queryset
|
||||
return paginator
|
||||
|
||||
def sort_queryset(self, queryset):
|
||||
sort_key = self.order.lstrip("-")
|
||||
if sort_key in self.sql_sort:
|
||||
queryset = queryset.order_by(self.order)
|
||||
elif sort_key == "name":
|
||||
queryset = queryset.order_by(self.order.replace("name", "i18n_name"))
|
||||
elif sort_key == "group":
|
||||
queryset = queryset.order_by(self.order + "__name")
|
||||
elif sort_key == "solved":
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
solved = user_completed_ids(profile)
|
||||
attempted = user_attempted_ids(profile)
|
||||
|
||||
def _solved_sort_order(problem):
|
||||
if problem.id in solved:
|
||||
return 1
|
||||
if problem.id in attempted:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=_solved_sort_order, reverse=self.order.startswith("-")
|
||||
)
|
||||
elif sort_key == "type":
|
||||
if self.show_types:
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=lambda problem: problem.types_list[0]
|
||||
if problem.types_list
|
||||
else "",
|
||||
reverse=self.order.startswith("-"),
|
||||
)
|
||||
return queryset
|
||||
|
||||
@cached_property
|
||||
def profile(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
|
@ -611,36 +619,28 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
self.request.GET.getlist("search")
|
||||
).strip()
|
||||
if query:
|
||||
if settings.ENABLE_FTS and self.full_text:
|
||||
queryset = queryset.search(query, queryset.BOOLEAN).extra(
|
||||
order_by=["-relevance"]
|
||||
substr_queryset = queryset.filter(
|
||||
Q(code__icontains=query)
|
||||
| Q(name__icontains=query)
|
||||
| Q(
|
||||
translations__name__icontains=query,
|
||||
translations__language=self.request.LANGUAGE_CODE,
|
||||
)
|
||||
)
|
||||
if settings.ENABLE_FTS:
|
||||
queryset = (
|
||||
queryset.search(query, queryset.BOOLEAN).extra(
|
||||
order_by=["-relevance"]
|
||||
)
|
||||
| substr_queryset
|
||||
)
|
||||
else:
|
||||
queryset = queryset.filter(
|
||||
Q(code__icontains=query)
|
||||
| Q(name__icontains=query)
|
||||
| Q(
|
||||
translations__name__icontains=query,
|
||||
translations__language=self.request.LANGUAGE_CODE,
|
||||
)
|
||||
)
|
||||
queryset = substr_queryset
|
||||
self.prepoint_queryset = queryset
|
||||
if self.point_start is not None:
|
||||
queryset = queryset.filter(points__gte=self.point_start)
|
||||
if self.point_end is not None:
|
||||
queryset = queryset.filter(points__lte=self.point_end)
|
||||
queryset = queryset.annotate(
|
||||
has_public_editorial=Case(
|
||||
When(
|
||||
solution__is_public=True,
|
||||
solution__publish_on__lte=timezone.now(),
|
||||
then=True,
|
||||
),
|
||||
default=False,
|
||||
output_field=BooleanField(),
|
||||
)
|
||||
)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -658,7 +658,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
context["show_types"] = 0 if self.in_contest else int(self.show_types)
|
||||
context["full_text"] = 0 if self.in_contest else int(self.full_text)
|
||||
context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial)
|
||||
context["have_editorial"] = 0 if self.in_contest else int(self.have_editorial)
|
||||
context["show_solved_only"] = (
|
||||
0 if self.in_contest else int(self.show_solved_only)
|
||||
)
|
||||
|
@ -768,7 +767,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
self.show_types = self.GET_with_session(request, "show_types")
|
||||
self.full_text = self.GET_with_session(request, "full_text")
|
||||
self.show_editorial = self.GET_with_session(request, "show_editorial")
|
||||
self.have_editorial = self.GET_with_session(request, "have_editorial")
|
||||
self.show_solved_only = self.GET_with_session(request, "show_solved_only")
|
||||
|
||||
self.search_query = None
|
||||
|
@ -816,7 +814,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
"show_types",
|
||||
"full_text",
|
||||
"show_editorial",
|
||||
"have_editorial",
|
||||
"show_solved_only",
|
||||
)
|
||||
for key in to_update:
|
||||
|
@ -862,9 +859,6 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
self.show_types = 1
|
||||
queryset = super(ProblemFeed, self).get_queryset()
|
||||
|
||||
if self.have_editorial:
|
||||
queryset = queryset.filter(has_public_editorial=1)
|
||||
|
||||
user = self.request.profile
|
||||
|
||||
if self.feed_type == "new":
|
||||
|
@ -886,6 +880,8 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
.order_by("?")
|
||||
.add_i18n_name(self.request.LANGUAGE_CODE)
|
||||
)
|
||||
if "search" in self.request.GET:
|
||||
return queryset.add_i18n_name(self.request.LANGUAGE_CODE)
|
||||
if not settings.ML_OUTPUT_PATH or not user:
|
||||
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
|
||||
|
||||
|
@ -946,7 +942,6 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
context["title"] = self.title
|
||||
context["feed_type"] = self.feed_type
|
||||
context["has_show_editorial_option"] = False
|
||||
context["has_have_editorial_option"] = False
|
||||
|
||||
return context
|
||||
|
||||
|
|
|
@ -31,10 +31,9 @@ class Resolver(TemplateView):
|
|||
for participation in self.contest.users.filter(virtual=0):
|
||||
cnt_user += 1
|
||||
users[str(cnt_user)] = {
|
||||
"username": participation.user.user.username,
|
||||
"name": participation.user.user.first_name
|
||||
or participation.user.user.username,
|
||||
"school": participation.user.user.last_name,
|
||||
"username": participation.user.username,
|
||||
"name": participation.user.first_name or participation.user.username,
|
||||
"school": participation.user.last_name,
|
||||
"last_submission": participation.cumtime_final,
|
||||
"problems": {},
|
||||
}
|
||||
|
|
|
@ -84,31 +84,7 @@ class SubmissionMixin(object):
|
|||
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
|
||||
def get_object(self, queryset=None):
|
||||
submission = super(SubmissionDetailBase, self).get_object(queryset)
|
||||
profile = self.request.profile
|
||||
problem = submission.problem
|
||||
if self.request.user.has_perm("judge.view_all_submission"):
|
||||
return submission
|
||||
if problem.is_public and self.request.user.has_perm(
|
||||
"judge.view_public_submission"
|
||||
):
|
||||
return submission
|
||||
if submission.user_id == profile.id:
|
||||
return submission
|
||||
if problem.is_editor(profile):
|
||||
return submission
|
||||
if problem.is_public or problem.testers.filter(id=profile.id).exists():
|
||||
if Submission.objects.filter(
|
||||
user_id=profile.id,
|
||||
result="AC",
|
||||
problem_id=problem.id,
|
||||
points=problem.points,
|
||||
).exists():
|
||||
return submission
|
||||
if hasattr(
|
||||
submission, "contest"
|
||||
) and submission.contest.participation.contest.is_editable_by(
|
||||
self.request.user
|
||||
):
|
||||
if submission.is_accessible_by(self.request.profile):
|
||||
return submission
|
||||
|
||||
raise PermissionDenied()
|
||||
|
@ -483,19 +459,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
authenticated = self.request.user.is_authenticated
|
||||
context["dynamic_update"] = False
|
||||
context["show_problem"] = self.show_problem
|
||||
context["completed_problem_ids"] = (
|
||||
user_completed_ids(self.request.profile) if authenticated else []
|
||||
)
|
||||
context["editable_problem_ids"] = (
|
||||
user_editable_ids(self.request.profile) if authenticated else []
|
||||
)
|
||||
context["tester_problem_ids"] = (
|
||||
user_tester_ids(self.request.profile) if authenticated else []
|
||||
)
|
||||
|
||||
context["profile"] = self.request.profile
|
||||
context["all_languages"] = Language.objects.all().values_list("key", "name")
|
||||
context["selected_languages"] = self.selected_languages
|
||||
|
||||
context["all_statuses"] = self.get_searchable_status_codes()
|
||||
context["selected_statuses"] = self.selected_statuses
|
||||
|
||||
|
@ -779,19 +745,10 @@ def single_submission(request, submission_id, show_problem=True):
|
|||
"submission/row.html",
|
||||
{
|
||||
"submission": submission,
|
||||
"completed_problem_ids": user_completed_ids(request.profile)
|
||||
if authenticated
|
||||
else [],
|
||||
"editable_problem_ids": user_editable_ids(request.profile)
|
||||
if authenticated
|
||||
else [],
|
||||
"tester_problem_ids": user_tester_ids(request.profile)
|
||||
if authenticated
|
||||
else [],
|
||||
"show_problem": show_problem,
|
||||
"problem_name": show_problem
|
||||
and submission.problem.translated_name(request.LANGUAGE_CODE),
|
||||
"profile_id": request.profile.id if authenticated else 0,
|
||||
"profile": request.profile if authenticated else None,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1010,9 +967,6 @@ class UserContestSubmissionsAjax(UserContestSubmissions):
|
|||
context["contest"] = self.contest
|
||||
context["problem"] = self.problem
|
||||
context["profile"] = self.profile
|
||||
context["profile_id"] = (
|
||||
self.request.profile.id if self.request.profile else None
|
||||
)
|
||||
|
||||
contest_problem = self.contest.contest_problems.get(problem=self.problem)
|
||||
filtered_submissions = []
|
||||
|
|
|
@ -35,6 +35,7 @@ from judge.utils.tickets import filter_visible_tickets, own_ticket_filter
|
|||
from judge.utils.views import SingleObjectFormView, TitleMixin, paginate_query_context
|
||||
from judge.views.problem import ProblemMixin
|
||||
from judge.widgets import HeavyPreviewPageDownWidget
|
||||
from judge.models.notification import make_notification
|
||||
|
||||
ticket_widget = (
|
||||
forms.Textarea()
|
||||
|
@ -49,16 +50,10 @@ ticket_widget = (
|
|||
|
||||
def add_ticket_notifications(users, author, link, ticket):
|
||||
html = f'<a href="{link}">{ticket.linked_item}</a>'
|
||||
|
||||
users = set(users)
|
||||
if author in users:
|
||||
users.remove(author)
|
||||
|
||||
for user in users:
|
||||
notification = Notification(
|
||||
owner=user, html_link=html, category="Ticket", author=author
|
||||
)
|
||||
notification.save()
|
||||
make_notification(users, "Ticket", html, author)
|
||||
|
||||
|
||||
class TicketForm(forms.Form):
|
||||
|
|
|
@ -52,11 +52,11 @@ else:
|
|||
class Media:
|
||||
css = {
|
||||
"all": [
|
||||
"markdown.css",
|
||||
"pagedown_widget.css",
|
||||
"content-description.css",
|
||||
"admin/css/pagedown.css",
|
||||
"pagedown.css",
|
||||
"https://fonts.googleapis.com/css2?family=Fira+Code&family=Noto+Sans&display=swap",
|
||||
]
|
||||
}
|
||||
js = ["admin/js/pagedown.js"]
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
padding-right: 15px !important;
|
||||
}
|
||||
|
||||
|
||||
.wmd-preview {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.md-typeset, .wmd-input {
|
||||
line-height: 1.4em !important;
|
||||
}
|
||||
|
|
|
@ -229,7 +229,6 @@
|
|||
.show-more {
|
||||
display: flex;
|
||||
color: black;
|
||||
font-style: italic;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
padding: 0px 12px;
|
||||
|
|
|
@ -40,6 +40,8 @@ a {
|
|||
.comment-img {
|
||||
display: flex;
|
||||
margin-right: 4px;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
|
||||
.new-comments .comment-display {
|
||||
|
@ -214,7 +216,7 @@ a {
|
|||
input, textarea {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
font-size: 1em;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2471,16 +2471,13 @@ input[type="text"], input[type="password"], input[type="email"], input[type="num
|
|||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset;
|
||||
}
|
||||
textarea {
|
||||
color: rgb(178, 172, 162);
|
||||
background-image: none;
|
||||
background-color: rgb(24, 26, 27);
|
||||
border-color: rgb(62, 68, 70);
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset;
|
||||
}
|
||||
textarea:hover {
|
||||
border-color: rgba(16, 87, 144, 0.8);
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset,
|
||||
rgba(16, 91, 150, 0.6) 0px 0px 4px;
|
||||
border-color: rgb(140, 130, 115);
|
||||
}
|
||||
input[type="text"]:hover, input[type="password"]:hover {
|
||||
border-color: rgba(16, 87, 144, 0.8);
|
||||
|
@ -2488,9 +2485,7 @@ input[type="text"]:hover, input[type="password"]:hover {
|
|||
rgba(16, 91, 150, 0.6) 0px 0px 4px;
|
||||
}
|
||||
textarea:focus {
|
||||
border-color: rgba(16, 87, 144, 0.8);
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset,
|
||||
rgba(16, 91, 150, 0.6) 0px 0px 8px; outline-color: initial;
|
||||
border-color: rgb(140, 130, 115); outline-color: initial;
|
||||
}
|
||||
input[type="text"]:focus, input[type="password"]:focus {
|
||||
border-color: rgba(16, 87, 144, 0.8);
|
||||
|
@ -2552,7 +2547,7 @@ input[type="text"]:focus, input[type="password"]:focus {
|
|||
ul.pagination a:hover {
|
||||
color: rgb(232, 230, 227);
|
||||
background-image: initial;
|
||||
background-color: rgb(8, 128, 104);
|
||||
background-color: rgb(163, 62, 18);
|
||||
border-color: initial;
|
||||
}
|
||||
ul.pagination > li > a,
|
||||
|
@ -2563,14 +2558,14 @@ ul.pagination > li > span {
|
|||
border-color: rgb(199, 70, 8);
|
||||
}
|
||||
ul.pagination > .disabled-page > a {
|
||||
color: rgb(157, 148, 136);
|
||||
background-color: rgba(3, 66, 54, 0.5);
|
||||
border-color: rgba(126, 117, 103, 0.5);
|
||||
color: rgb(223, 220, 215);
|
||||
background-color: rgb(137, 78, 57);
|
||||
border-color: rgb(199, 68, 21);
|
||||
}
|
||||
ul.pagination > .disabled-page > span {
|
||||
color: rgb(157, 148, 136);
|
||||
background-color: rgba(3, 66, 54, 0.5);
|
||||
border-color: rgba(126, 117, 103, 0.5);
|
||||
color: rgb(223, 220, 215);
|
||||
background-color: rgb(137, 78, 57);
|
||||
border-color: rgb(199, 68, 21);
|
||||
}
|
||||
ul.pagination > .active-page > a {
|
||||
color: rgb(232, 230, 227);
|
||||
|
|
|
@ -13,7 +13,7 @@ div.dmmd-preview-update {
|
|||
}
|
||||
|
||||
div.dmmd-preview-content {
|
||||
padding: 0 7px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update {
|
||||
|
@ -21,7 +21,8 @@ div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update {
|
|||
}
|
||||
|
||||
div.dmmd-preview-has-content div.dmmd-preview-content {
|
||||
padding-bottom: 7px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
div.dmmd-no-button div.dmmd-preview-update {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
width: 100%;
|
||||
background: #fff;
|
||||
border: 1px solid DarkGray;
|
||||
font-family: Consolas, "Liberation Mono", Monaco, "Courier New", monospace !important;
|
||||
font-family: "Noto Sans",Arial,"Lucida Grande",sans-serif !important;
|
||||
}
|
||||
|
||||
.wmd-preview {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
width: 100%;
|
||||
background: #fff;
|
||||
border: 1px solid DarkGray;
|
||||
font-family: Consolas, "Liberation Mono", Monaco, "Courier New", monospace !important;
|
||||
font-family: "Noto Sans",Arial,"Lucida Grande",sans-serif !important;
|
||||
}
|
||||
|
||||
.wmd-preview {
|
||||
|
@ -174,51 +174,51 @@
|
|||
|
||||
/* Extra styles to allow for image upload */
|
||||
.pagedown-image-upload {
|
||||
display: none;
|
||||
z-index: 10001;
|
||||
position: fixed;
|
||||
background: white;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
padding: 10px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
z-index: 10001;
|
||||
position: fixed;
|
||||
background: white;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
padding: 10px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.pagedown-image-upload .submit-row {
|
||||
margin: 10px 0 0 0;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.pagedown-image-upload.show {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pagedown-image-upload .submit-loading {
|
||||
display: none;
|
||||
vertical-align: middle;
|
||||
border: 4px solid #f3f3f3; /* Light grey */
|
||||
border-top: 4px solid #79aec8; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: none;
|
||||
vertical-align: middle;
|
||||
border: 4px solid #f3f3f3; /* Light grey */
|
||||
border-top: 4px solid #79aec8; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.pagedown-image-upload .submit-loading.show {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pagedown-image-upload .submit-input {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedown-image-upload .submit-input.show {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
|
@ -161,8 +161,7 @@ input {
|
|||
}
|
||||
|
||||
textarea {
|
||||
padding: 4px 8px;
|
||||
color: #555;
|
||||
padding: 8px;
|
||||
background: #FFF none;
|
||||
border: 1px solid $border_gray;
|
||||
border-radius: $widget_border_radius;
|
||||
|
@ -172,8 +171,7 @@ textarea {
|
|||
}
|
||||
|
||||
textarea:hover {
|
||||
border-color: rgba(82, 168, 236, 0.8);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 4px rgba(82, 168, 236, 0.6);
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
input {
|
||||
|
@ -184,8 +182,8 @@ input {
|
|||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: rgba(82, 168, 236, 0.8);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||
border-color: black;
|
||||
border-width: unset;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
|
@ -322,7 +320,7 @@ input {
|
|||
// Bootstrap-y pagination
|
||||
ul.pagination a:hover {
|
||||
color: #FFF;
|
||||
background: #0aa082;
|
||||
background: #cc4e17;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
@ -338,22 +336,6 @@ ul.pagination {
|
|||
li {
|
||||
display: inline;
|
||||
|
||||
// &:first-child > {
|
||||
// a, span {
|
||||
// margin-left: 0;
|
||||
// border-top-left-radius: $widget_border_radius;
|
||||
// border-bottom-left-radius: $widget_border_radius;
|
||||
// }
|
||||
// }
|
||||
|
||||
// &:last-child > {
|
||||
// a, span {
|
||||
// margin-left: 0;
|
||||
// border-top-right-radius: $widget_border_radius;
|
||||
// border-bottom-right-radius: $widget_border_radius;
|
||||
// }
|
||||
// }
|
||||
|
||||
> {
|
||||
a, span {
|
||||
position: relative;
|
||||
|
@ -373,15 +355,15 @@ ul.pagination {
|
|||
|
||||
.disabled-page > {
|
||||
a {
|
||||
color: #888;
|
||||
background-color: #04534380;
|
||||
border-color: #04534380;
|
||||
color: #f1efef;
|
||||
background-color: #ab6247;
|
||||
border-color: #6a240b;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #888;
|
||||
background-color: #04534380;
|
||||
border-color: #04534380;
|
||||
color: #f1efef;
|
||||
background-color: #ab6247;
|
||||
border-color: #6a240b;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,13 +18,20 @@
|
|||
width: 100% !important;
|
||||
}
|
||||
|
||||
#content input.select2-search__field {
|
||||
border: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#content .content-description h1,
|
||||
#content .content-description h2,
|
||||
#content .content-description h3,
|
||||
#content .content-description h4,
|
||||
#content .content-description h5,
|
||||
#content .content-description h6 {
|
||||
padding: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#content .content-description h5 {
|
||||
|
@ -32,14 +39,16 @@
|
|||
text-transform: initial;
|
||||
}
|
||||
|
||||
#content input.select2-search__field {
|
||||
border: none;
|
||||
box-shadow: none !important;
|
||||
#content .content-description ul,
|
||||
#content .content-description ol {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#content .content-description ul,
|
||||
#content .content-description ol {
|
||||
margin: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#content .content-description li {
|
||||
|
|
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 %}
|
|
@ -13,14 +13,14 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block user_footer %}
|
||||
{% if user.user.first_name %}
|
||||
{% if user.first_name %}
|
||||
<div style="font-weight: 600; display: none" class="fullname gray">
|
||||
{{ user.user.first_name if user.user.first_name else ''}}
|
||||
{{ user.first_name if user.first_name else ''}}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user.user.last_name %}
|
||||
{% if user.last_name %}
|
||||
<div class="school gray" style="display: none"><a style="font-weight: 600">
|
||||
{{- user.user.last_name -}}
|
||||
{{- user.last_name -}}
|
||||
</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="has_next" style="display: none;" value="{{1 if has_next_page else 0}}"></div>
|
||||
{% if has_next_page %}
|
||||
<button class="view-next-page btn-green small">{{_('View more')}}</button>
|
||||
<button class="view-next-page">{{_('View more')}}</button>
|
||||
{% endif %}
|
|
@ -1,11 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% if not has_notifications %}
|
||||
|
||||
<h2 style="text-align: center;">{{ _('You have no notifications') }}</h2>
|
||||
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
|
@ -17,24 +14,15 @@
|
|||
{% for notification in notifications %}
|
||||
<tr class="{{ 'highlight' if not notification.seen }}">
|
||||
<td>
|
||||
{% if notification.comment %}
|
||||
{{ link_user(notification.comment.author) }}
|
||||
{% else %}
|
||||
{{ link_user(notification.author) }}
|
||||
{% endif %}
|
||||
|
||||
{{ link_user(notification.author) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ notification.category }}
|
||||
</td>
|
||||
<td>
|
||||
{% if notification.comment %}
|
||||
<a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ notification.comment.page_title }}</a>
|
||||
{% else %}
|
||||
{% autoescape off %}
|
||||
{{notification.html_link}}
|
||||
{% endautoescape %}
|
||||
{% endif %}
|
||||
{% autoescape off %}
|
||||
{{notification.html_link}}
|
||||
{% endautoescape %}
|
||||
</td>
|
||||
<td>
|
||||
{{ relative_time(notification.time) }}
|
||||
|
@ -43,8 +31,5 @@
|
|||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<!--
|
||||
-->
|
|
@ -114,7 +114,7 @@
|
|||
|
||||
$('#go').click(clean_submit);
|
||||
|
||||
$('input#full_text, input#hide_solved, input#show_types, input#show_editorial, input#have_editorial, input#show_solved_only').click(function () {
|
||||
$('input#full_text, input#hide_solved, input#show_types, input#have_editorial, input#show_solved_only').click(function () {
|
||||
prep_form();
|
||||
($('<form>').attr('action', window.location.pathname + '?' + form_serialize())
|
||||
.append($('<input>').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% if related_problems %}
|
||||
{% if request.user.is_authenticated and related_problems %}
|
||||
<div class="content-description">
|
||||
<h4>{{_('Recommended problems')}}:</h4>
|
||||
<ul style="list-style-type: none; margin: 0.3em;">
|
||||
|
|
|
@ -34,13 +34,6 @@
|
|||
<label for="show_editorial">{{ _('Show editorial') }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_have_editorial_option %}
|
||||
<div>
|
||||
<input id="have_editorial" type="checkbox" name="have_editorial" value="1"
|
||||
{% if have_editorial %} checked{% endif %}>
|
||||
<label for="have_editorial">{{ _('Have editorial') }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if organizations %}
|
||||
<div class="filter-form-group">
|
||||
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids, tester_problem_ids) %}
|
||||
{% set can_view = submission.is_accessible_by(profile) %}
|
||||
<div class="sub-result {{ submission._result_class if in_hidden_subtasks_contest else submission.result_class }}">
|
||||
<div class="score">
|
||||
{%- if submission.is_graded -%}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<h4>
|
||||
{{_('Contest submissions of')}} {{link_user(profile)}} <a href="{{url('contest_user_submissions', contest.key, profile.user.username, problem.code)}}">#</a>
|
||||
{{_('Contest submissions of')}} {{link_user(profile)}} <a href="{{url('contest_user_submissions', contest.key, profile.username, problem.code)}}">#</a>
|
||||
</h4>
|
||||
<hr>
|
||||
{% if best_subtasks and subtasks %}
|
||||
|
@ -19,7 +19,7 @@
|
|||
</td>
|
||||
{% endif %}
|
||||
{% if cur_subtask.submission %}
|
||||
{% set can_view = submission_layout(cur_subtask.submission, profile_id, request.user, editable_problem_ids, completed_problem_ids, tester_problem_ids) %}
|
||||
{% set can_view = cur_subtask.submission.is_accessible_by(profile) %}
|
||||
{% if can_view %}
|
||||
<td>
|
||||
→ <a href="{{url('submission_status', cur_subtask.submission.id)}}">{{ cur_subtask.submission.id }}</a>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<table class="lightbox-submissions"><tbody>
|
||||
{% for submission in submissions %}
|
||||
<tr>
|
||||
{% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids, tester_problem_ids) %}
|
||||
{% set can_view = submission.is_accessible_by(profile) %}
|
||||
<td class="lightbox-submissions-time">
|
||||
{% if submission.contest_time %}
|
||||
{{submission.contest_time}}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<span class="{{ profile.css_class }}"><a href="{{ url('user_page', user.username) }}">{{ user.username }}</a></span>
|
||||
<span class="{{ profile.css_class }}"><a href="{{ url('user_page', profile.username) }}">{{ profile.username }}</a></span>
|
||||
|
|
|
@ -54,9 +54,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.user.first_name %}
|
||||
{% if user.first_name %}
|
||||
<p style="font-size:1.4em; text-align: center;">
|
||||
{{user.user.first_name}}{% if user.user.last_name %} ({{user.user.last_name}}){% endif %}
|
||||
{{user.first_name}}{% if user.last_name %} ({{user.last_name}}){% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if perms.judge.change_profile %}
|
||||
|
@ -70,9 +70,7 @@
|
|||
{% endif%}
|
||||
{% if user.about %}
|
||||
<h4>{{ _('About') }}</h4>
|
||||
{% cache 86400 'user_about' user.id MATH_ENGINE %}
|
||||
{{ user.about|markdown(lazy_load=True)|reference|str|safe }}
|
||||
{% endcache %}
|
||||
{{ user.about|markdown(lazy_load=True)|reference|str|safe }}
|
||||
{% else %}
|
||||
<i>
|
||||
{% if user.user == request.user %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
{% block user_footer %}
|
||||
<span style="margin-left: 0.5em">{% if user.rating %}{{ rating_number(user) }}{% endif %}</span>
|
||||
<div class="gray" style="font-weight: 600; margin-top: 0.2em">{{ user.user.first_name if user.user.first_name else ''}}</div>
|
||||
<div class="gray" style="font-weight: 600; margin-top: 0.2em">{{ user.first_name or ''}}</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block after_point_head %}
|
||||
|
@ -39,11 +39,11 @@
|
|||
<td class="user-problem-count">{{ user.problem_count }}</td>
|
||||
<td>
|
||||
<div class="about-td">
|
||||
{% if user.about %}
|
||||
{% cache 86400 'user_about' user.id MATH_ENGINE %}
|
||||
{% cache 86400 'user_about' user.id MATH_ENGINE %}
|
||||
{% if user.about %}
|
||||
{{ user.about|markdown(lazy_load=True)|reference|str|safe }}
|
||||
{% endcache %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
</td>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue