diff --git a/chat_box/models.py b/chat_box/models.py index fb3fd7b..fb6de76 100644 --- a/chat_box/models.py +++ b/chat_box/models.py @@ -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: diff --git a/chat_box/utils.py b/chat_box/utils.py index dd59d98..f3f27b4 100644 --- a/chat_box/utils.py +++ b/chat_box/utils.py @@ -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 = ( diff --git a/chat_box/views.py b/chat_box/views.py index 5a15f74..48fa37a 100644 --- a/chat_box/views.py +++ b/chat_box/views.py @@ -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"}) diff --git a/dmoj/urls.py b/dmoj/urls.py index 4e0691e..e6610f8 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -517,6 +517,11 @@ urlpatterns = [ ), ), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), + url( + r"^contests/summary/(?P\w+)$", + contests.contests_summary_view, + name="contests_summary", + ), url(r"^course/", paged_list_view(course.CourseList, "course_list")), url( r"^contests/(?P\d+)/(?P\d+)/$", diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 94af5aa..05032d6 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -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) diff --git a/judge/admin/contest.py b/judge/admin/contest.py index a126e83..b7b5457 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -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 diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 49a145d..7f0e473 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -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"): diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 422fc5d..68ecee1 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -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") diff --git a/judge/caching.py b/judge/caching.py index 42e9311..43479da 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -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 diff --git a/judge/comments.py b/judge/comments.py index 65d8f7e..dde97a0 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -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'{comment.page_title}' + + 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) diff --git a/judge/jinja2/__init__.py b/judge/jinja2/__init__.py index 93d0ed5..93ab0ad 100644 --- a/judge/jinja2/__init__.py +++ b/judge/jinja2/__init__.py @@ -21,7 +21,6 @@ from . import ( render, social, spaceless, - submission, timedelta, ) from . import registry diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index cffa413..b6e8a83 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -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() diff --git a/judge/jinja2/reference.py b/judge/jinja2/reference.py index 49a6ef2..312ac4f 100644 --- a/judge/jinja2/reference.py +++ b/judge/jinja2/reference.py @@ -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 diff --git a/judge/jinja2/submission.py b/judge/jinja2/submission.py deleted file mode 100644 index cdb3634..0000000 --- a/judge/jinja2/submission.py +++ /dev/null @@ -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 diff --git a/judge/migrations/0170_contests_summary.py b/judge/migrations/0170_contests_summary.py new file mode 100644 index 0000000..a2b19de --- /dev/null +++ b/judge/migrations/0170_contests_summary.py @@ -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")), + ], + ), + ] diff --git a/judge/migrations/0171_update_notification.py b/judge/migrations/0171_update_notification.py new file mode 100644 index 0000000..7803e4b --- /dev/null +++ b/judge/migrations/0171_update_notification.py @@ -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'{c.comment.page_title}' + ) + 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" + ), + ), + ], + ), + ] diff --git a/judge/migrations/0172_index_rating.py b/judge/migrations/0172_index_rating.py new file mode 100644 index 0000000..157c005 --- /dev/null +++ b/judge/migrations/0172_index_rating.py @@ -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), + ), + ] diff --git a/judge/migrations/0173_fulltext.py b/judge/migrations/0173_fulltext.py new file mode 100644 index 0000000..a47ef7b --- /dev/null +++ b/judge/migrations/0173_fulltext.py @@ -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, + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index ee9d364..2b1d706 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -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"]) diff --git a/judge/models/comment.py b/judge/models/comment.py index 6058cf8..2cbe20a 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -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, - ) diff --git a/judge/models/contest.py b/judge/models/contest.py index a724f1a..bda262d 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -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]) diff --git a/judge/models/notification.py b/judge/models/notification.py new file mode 100644 index 0000000..01c5b0d --- /dev/null +++ b/judge/models/notification.py @@ -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 diff --git a/judge/models/pagevote.py b/judge/models/pagevote.py index 5e74c95..7accd01 100644 --- a/judge/models/pagevote.py +++ b/judge/models/pagevote.py @@ -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(): diff --git a/judge/models/problem.py b/judge/models/problem.py index 3377b2d..47741e9 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -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): diff --git a/judge/models/profile.py b/judge/models/profile.py index 96f3cfd..3692e39 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -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) diff --git a/judge/models/submission.py b/judge/models/submission.py index fea9b64..00c5941 100644 --- a/judge/models/submission.py +++ b/judge/models/submission.py @@ -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"), diff --git a/judge/utils/problems.py b/judge/utils/problems.py index 6c351aa..67ef163 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -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: { diff --git a/judge/views/comment.py b/judge/views/comment.py index 6965201..caa4eb6 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -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(): diff --git a/judge/views/contests.py b/judge/views/contests.py index 97ff251..a84d98b 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -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) diff --git a/judge/views/notification.py b/judge/views/notification.py index 63f38c2..bb79317 100644 --- a/judge/views/notification.py +++ b/judge/views/notification.py @@ -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 diff --git a/judge/views/organization.py b/judge/views/organization.py index 0576afe..be80203 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -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'{self.object.title} - {self.organization.name}' ) - 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'{blog.title} - {self.organization.name}' 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(): diff --git a/judge/views/pagevote.py b/judge/views/pagevote.py index 9ae6d5b..988d355 100644 --- a/judge/views/pagevote.py +++ b/judge/views/pagevote.py @@ -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) diff --git a/judge/views/problem.py b/judge/views/problem.py index 2be9cb4..0bf304b 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -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 diff --git a/judge/views/resolver.py b/judge/views/resolver.py index eade951..cf8a193 100644 --- a/judge/views/resolver.py +++ b/judge/views/resolver.py @@ -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": {}, } diff --git a/judge/views/submission.py b/judge/views/submission.py index 1556dd9..eae961b 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -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 = [] diff --git a/judge/views/ticket.py b/judge/views/ticket.py index 3d62235..60a151e 100644 --- a/judge/views/ticket.py +++ b/judge/views/ticket.py @@ -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'{ticket.linked_item}' - 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): diff --git a/judge/widgets/pagedown.py b/judge/widgets/pagedown.py index edc4bf1..c30940b 100644 --- a/judge/widgets/pagedown.py +++ b/judge/widgets/pagedown.py @@ -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"] diff --git a/resources/admin/css/pagedown.css b/resources/admin/css/pagedown.css index 27732de..ee29196 100644 --- a/resources/admin/css/pagedown.css +++ b/resources/admin/css/pagedown.css @@ -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; +} diff --git a/resources/blog.scss b/resources/blog.scss index ad21448..4c57cfe 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -229,7 +229,6 @@ .show-more { display: flex; color: black; - font-style: italic; font-size: 16px; font-weight: 700; padding: 0px 12px; diff --git a/resources/comments.scss b/resources/comments.scss index c606f26..05ef84f 100644 --- a/resources/comments.scss +++ b/resources/comments.scss @@ -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; } } diff --git a/resources/darkmode-svg.css b/resources/darkmode-svg.css index 83c1f21..7e8f329 100644 --- a/resources/darkmode-svg.css +++ b/resources/darkmode-svg.css @@ -14,3 +14,9 @@ .wmd-heading-button { background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB3aWR0aD0iMTc5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBzdHlsZT0iZmlsdGVyOmludmVydCgxMDAlKSIgZD0iTTE2ODIgMTY2NHEtNDQgMC0xMzIuNS0zLjV0LTEzMy41LTMuNXEtNDQgMC0xMzIgMy41dC0xMzIgMy41cS0yNCAwLTM3LTIwLjV0LTEzLTQ1LjVxMC0zMSAxNy00NnQzOS0xNyA1MS03IDQ1LTE1cTMzLTIxIDMzLTE0MGwtMS0zOTFxMC0yMS0xLTMxLTEzLTQtNTAtNGgtNjc1cS0zOCAwLTUxIDQtMSAxMC0xIDMxbC0xIDM3MXEwIDE0MiAzNyAxNjQgMTYgMTAgNDggMTN0NTcgMy41IDQ1IDE1IDIwIDQ1LjVxMCAyNi0xMi41IDQ4dC0zNi41IDIycS00NyAwLTEzOS41LTMuNXQtMTM4LjUtMy41cS00MyAwLTEyOCAzLjV0LTEyNyAzLjVxLTIzIDAtMzUuNS0yMXQtMTIuNS00NXEwLTMwIDE1LjUtNDV0MzYtMTcuNSA0Ny41LTcuNSA0Mi0xNXEzMy0yMyAzMy0xNDNsLTEtNTd2LTgxM3EwLTMgLjUtMjZ0MC0zNi41LTEuNS0zOC41LTMuNS00Mi02LjUtMzYuNS0xMS0zMS41LTE2LTE4cS0xNS0xMC00NS0xMnQtNTMtMi00MS0xNC0xOC00NXEwLTI2IDEyLTQ4dDM2LTIycTQ2IDAgMTM4LjUgMy41dDEzOC41IDMuNXE0MiAwIDEyNi41LTMuNXQxMjYuNS0zLjVxMjUgMCAzNy41IDIydDEyLjUgNDhxMCAzMC0xNyA0My41dC0zOC41IDE0LjUtNDkuNSA0LTQzIDEzcS0zNSAyMS0zNSAxNjBsMSAzMjBxMCAyMSAxIDMyIDEzIDMgMzkgM2g2OTlxMjUgMCAzOC0zIDEtMTEgMS0zMmwxLTMyMHEwLTEzOS0zNS0xNjAtMTgtMTEtNTguNS0xMi41dC02Ni0xMy0yNS41LTQ5LjVxMC0yNiAxMi41LTQ4dDM3LjUtMjJxNDQgMCAxMzIgMy41dDEzMiAzLjVxNDMgMCAxMjktMy41dDEyOS0zLjVxMjUgMCAzNy41IDIydDEyLjUgNDhxMCAzMC0xNy41IDQ0dC00MCAxNC41LTUxLjUgMy00NCAxMi41cS0zNSAyMy0zNSAxNjFsMSA5NDNxMCAxMTkgMzQgMTQwIDE2IDEwIDQ2IDEzLjV0NTMuNSA0LjUgNDEuNSAxNS41IDE4IDQ0LjVxMCAyNi0xMiA0OHQtMzYgMjJ6Ii8+PC9zdmc+"); } +.wmd-undo-button { + background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI0OHB4IiBpZD0ic3ZnMTEzMDAiIHdpZHRoPSI0OHB4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnMgaWQ9ImRlZnMzIj48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50MjMyNiI+PHN0b3AgaWQ9InN0b3AyMzI4IiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNmZmZmZmY7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3AyMzMwIiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNmZmZmZmY7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50MjMxNiI+PHN0b3AgaWQ9InN0b3AyMzE4IiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNjNGEwMDA7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3AyMzIwIiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNjNGEwMDA7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50MjMwOCI+PHN0b3AgaWQ9InN0b3AyMzEwIiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNlZGQ0MDA7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3AyMzEyIiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNlZGQ0MDA7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50ODY2MiI+PHN0b3AgaWQ9InN0b3A4NjY0IiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiMwMDAwMDA7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3A4NjY2IiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiMwMDAwMDA7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgY3g9IjI0LjgzNzEyNiIgY3k9IjM2LjQyMTEyNyIgZng9IjI0LjgzNzEyNiIgZnk9IjM2LjQyMTEyNyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLjAwMDAwMCwwLjAwMDAwMCwwLjAwMDAwMCwwLjUzNjcyMywtNi4yMjcyNjVlLTE0LDE2Ljg3MzA2KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJyYWRpYWxHcmFkaWVudDg2NjgiIHI9IjE1LjY0NDczNyIgeGxpbms6aHJlZj0iI2xpbmVhckdyYWRpZW50ODY2MiIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQyMTg3Ij48c3RvcCBpZD0ic3RvcDIxODkiIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6I2ZmZmZmZjtzdG9wLW9wYWNpdHk6MTsiLz48c3RvcCBpZD0ic3RvcDIxOTEiIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I2ZmZmZmZjtzdG9wLW9wYWNpdHk6MDsiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC0xLjgxMzQ3MWUtMTYsLTEuMTcxOTI2LC0xLjE3MTkyNiwxLjgxMzQ3MWUtMTYsNDYuMTc0NDAsNTQuMTAxMTEpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImxpbmVhckdyYWRpZW50MTc2NCIgeDE9IjE3LjA2MDgwNiIgeDI9IjEyLjYyNDMzNyIgeGxpbms6aHJlZj0iI2xpbmVhckdyYWRpZW50MjE4NyIgeTE9IjExLjM5NTAyIiB5Mj0iMTIuNTgzNzY5Ii8+PGxpbmVhckdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0ibGluZWFyR3JhZGllbnQyMzE0IiB4MT0iMjYuNSIgeDI9IjI2LjI1IiB4bGluazpocmVmPSIjbGluZWFyR3JhZGllbnQyMzA4IiB5MT0iMzQuMjUiIHkyPSI0My41NzE4MzEiLz48bGluZWFyR3JhZGllbnQgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJsaW5lYXJHcmFkaWVudDIzMjIiIHgxPSIyNi41IiB4Mj0iMjYuMjUiIHhsaW5rOmhyZWY9IiNsaW5lYXJHcmFkaWVudDIzMTYiIHkxPSIzNC4yNSIgeTI9IjQzLjU3MTgzMSIvPjxyYWRpYWxHcmFkaWVudCBjeD0iMTUuMDk0MDMiIGN5PSIxMy4yODI3MjEiIGZ4PSIxNS4wOTQwMyIgZnk9IjEzLjI4MjcyMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyLjQ5NjAzMSwtMS4xNTE5MDVlLTE2LDEuMDYxNzU2ZS0xNiwyLjMwMDY4OSwtMjUuMTI0MDIsLTE3LjgyNjM2KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJyYWRpYWxHcmFkaWVudDIzMzIiIHI9IjEwLjE2NDY2IiB4bGluazpocmVmPSIjbGluZWFyR3JhZGllbnQyMzI2Ii8+PC9kZWZzPjxnIGlkPSJsYXllcjEiPjxwYXRoIGQ9Ik0gNDAuNDgxODYzIDM2LjQyMTEyNyBBIDE1LjY0NDczNyA4LjM5Njg5MzUgMCAxIDEgIDkuMTkyMzg4NSwzNi40MjExMjcgQSAxNS42NDQ3MzcgOC4zOTY4OTM1IDAgMSAxICA0MC40ODE4NjMgMzYuNDIxMTI3IHoiIGlkPSJwYXRoODY2MCIgc3R5bGU9ImludmVydDoxMDAlO29wYWNpdHk6MC4xNDExNzY0Nztjb2xvcjojMDAwMDAwO2ZpbGw6dXJsKCNyYWRpYWxHcmFkaWVudDg2NjgpO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO21hcmtlcjpub25lO21hcmtlci1zdGFydDpub25lO21hcmtlci1taWQ6bm9uZTttYXJrZXItZW5kOm5vbmU7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZSIgdHJhbnNmb3JtPSJtYXRyaXgoLTEuNDg5NzM2LDAuMDAwMDAwLDAuMDAwMDAwLC0xLjAwMTI1Miw2MC42MDQzNiw3NS4zMTI2MCkiLz48cGF0aCBkPSJNIDkuNTgyNDQxLDQ1LjAzNDM2OSBDIDQ5LjYwODI0OSw0Ni4zNTU1MDkgNDMuMjgyNDA1LDEyLjI5MzU1IDIyLjQ2MjQxMSwxMi40OTc2NSBMIDIyLjQ2MjQxMSwzLjEyMjIzOTYgTCA1LjgxMzkyOTgsMTcuNzA4ODE5IEwgMjIuNDYyNDExLDMzLjAwNjM0OSBDIDIyLjQ2MjQxMSwzMy4wMDYzNDkgMjIuNDYyNDExLDIzLjMzNzk2OSAyMi40NjI0MTEsMjMuMzM3OTY5IEMgMzYuNTI1NTIxLDIyLjc1MTk5OSA0MC42Mzk5MzksNDQuNzcwNTQ5IDkuNTgyNDQxLDQ1LjAzNDM2OSB6ICIgaWQ9InBhdGgxNDMyIiBzdHlsZT0iaW52ZXJ0OjEwMCU7b3BhY2l0eToxO2NvbG9yOiMwMDAwMDA7ZmlsbDp1cmwoI2xpbmVhckdyYWRpZW50MjMxNCk7ZmlsbC1vcGFjaXR5OjEuMDtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6dXJsKCNsaW5lYXJHcmFkaWVudDIzMjIpO3N0cm9rZS13aWR0aDoxLjAwMDAwMDEyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO21hcmtlcjpub25lO21hcmtlci1zdGFydDpub25lO21hcmtlci1taWQ6bm9uZTttYXJrZXItZW5kOm5vbmU7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmJsb2NrO292ZXJmbG93OnZpc2libGUiLz48cGF0aCBkPSJNIDMxLjAzMjI4MSwzOS4zMTU1MTkgQyA0Mi43NTUzOCwzMy4yMzU4OTIgMzkuMjIwMDczLDEzLjA4NzQ4OSAyMS40NDg3MDEsMTMuNTQ5OTU5IEwgMjEuNDQ4NzAxLDUuNDUwODY3OCBDIDIxLjQ0ODcwMSw1LjQ1MDg2NzggNy40MDA5NjI4LDE3LjcxNDU4OSA3LjQwMDk2MjgsMTcuNzE0NTg5IEwgMjEuNDQ4NzAxLDMwLjY1ODYxNyBDIDIxLjQ0ODcwMSwzMC42NTg2MTcgMjEuNDQ4NzAxLDIyLjM4MDk3OSAyMS40NDg3MDEsMjIuMzgwOTc5IEMgMzYuMjg4NTUxLDIyLjAzMjcwOSAzNS42MDg2MTEsMzUuMTM4NTc5IDMxLjAzMjI4MSwzOS4zMTU1MTkgeiAiIGlkPSJwYXRoMjE3NyIgc3R5bGU9ImludmVydDoxMDAlO29wYWNpdHk6MC42OTg4NjM2MTtjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6dXJsKCNsaW5lYXJHcmFkaWVudDE3NjQpO3N0cm9rZS13aWR0aDowLjk5OTk5OTc7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7bWFya2VyLXN0YXJ0Om5vbmU7bWFya2VyLW1pZDpub25lO21hcmtlci1lbmQ6bm9uZTtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmJsb2NrO292ZXJmbG93OnZpc2libGUiLz48cGF0aCBkPSJNIDYuNjI5MTI2MSwxNy42ODI3OTcgTCAxMi4yODU5OCwyMy4wNzQ0ODYgQyAxOC41NjE1NTMsMjIuODk3NzA5IDE1LjczMzEyNiwxNi43MTA1MjUgMjYuOTU4NDQ2LDEzLjYxNjkzMyBMIDIyLjAwODY5OSwxMi45OTgyMTQgTCAyMS45MjAzMSw0LjMzNjE1NjIgTCA2LjYyOTEyNjEsMTcuNjgyNzk3IHogIiBpZD0icGF0aDIzMjQiIHN0eWxlPSJpbnZlcnQ6MTAwJTtvcGFjaXR5OjAuNTExMzYzNjQ7Y29sb3I6IzAwMDAwMDtmaWxsOnVybCgjcmFkaWFsR3JhZGllbnQyMzMyKTtmaWxsLW9wYWNpdHk6MS4wO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO21hcmtlcjpub25lO21hcmtlci1zdGFydDpub25lO21hcmtlci1taWQ6bm9uZTttYXJrZXItZW5kOm5vbmU7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlIi8+PC9nPjwvc3ZnPg=="); +} +.wmd-redo-button { + background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI0OHB4IiBpZD0ic3ZnMTEzMDAiIHdpZHRoPSI0OHB4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnMgaWQ9ImRlZnMzIj48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50MjI0MCI+PHN0b3AgaWQ9InN0b3AyMjQyIiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiM5OWIwMGI7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3AyMjQ0IiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiM5OWIwMGI7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50MjIzMiI+PHN0b3AgaWQ9InN0b3AyMjM0IiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiM3ODg2MDA7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3AyMjM2IiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiM3ODg2MDA7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NDk5MSI+PHN0b3AgaWQ9InN0b3A0OTkzIiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNmZmZmZmY7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3A0OTk1IiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNmZmZmZmY7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50ODY2MiI+PHN0b3AgaWQ9InN0b3A4NjY0IiBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiMwMDAwMDA7c3RvcC1vcGFjaXR5OjE7Ii8+PHN0b3AgaWQ9InN0b3A4NjY2IiBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiMwMDAwMDA7c3RvcC1vcGFjaXR5OjA7Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgY3g9IjI0LjgzNzEyNiIgY3k9IjM2LjQyMTEyNyIgZng9IjI0LjgzNzEyNiIgZnk9IjM2LjQyMTEyNyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLjAwMDAwMCwwLjAwMDAwMCwwLjAwMDAwMCwwLjUzNjcyMywtNS44MjUzMjllLTE0LDE2Ljg3MzA2KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJyYWRpYWxHcmFkaWVudDg2NjgiIHI9IjE1LjY0NDczNyIgeGxpbms6aHJlZj0iI2xpbmVhckdyYWRpZW50ODY2MiIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQyMTg3Ij48c3RvcCBpZD0ic3RvcDIxODkiIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6I2ZmZmZmZjtzdG9wLW9wYWNpdHk6MTsiLz48c3RvcCBpZD0ic3RvcDIxOTEiIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I2ZmZmZmZjtzdG9wLW9wYWNpdHk6MDsiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEuODEzNDcxZS0xNiwtMS4xNzE5MjYsMS4xNzE5MjYsMS44MTM0NzFlLTE2LDEuNzgyODAxLDU0LjEwMTExKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJsaW5lYXJHcmFkaWVudDE3NjQiIHgxPSIxNy4wNjA4MDYiIHgyPSIxMi42MjQzMzciIHhsaW5rOmhyZWY9IiNsaW5lYXJHcmFkaWVudDIxODciIHkxPSIxMS4zOTUwMiIgeTI9IjEyLjU4Mzc2OSIvPjxyYWRpYWxHcmFkaWVudCBjeD0iMTYuNTYzODM3IiBjeT0iMTEuMTMyMjM2IiBmeD0iMTYuNTYzODM3IiBmeT0iMTEuMTMyMjM2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC0xLjI5MDEyN2UtMiwxLjY4NTE5NywxLjcxMzA4MiwxLjMxMTQ3NWUtMiwtMS4wNDE0OTksLTEwLjExNTcxKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJyYWRpYWxHcmFkaWVudDQ5OTciIHI9IjE5LjA2MjUiIHhsaW5rOmhyZWY9IiNsaW5lYXJHcmFkaWVudDQ5OTEiLz48bGluZWFyR3JhZGllbnQgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJsaW5lYXJHcmFkaWVudDIyMzgiIHgxPSIzMyIgeDI9IjMxLjUiIHhsaW5rOmhyZWY9IiNsaW5lYXJHcmFkaWVudDIyMzIiIHkxPSIzNS43NSIgeTI9IjQyLjUiLz48bGluZWFyR3JhZGllbnQgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJsaW5lYXJHcmFkaWVudDIyNDYiIHgxPSIzMyIgeDI9IjMxLjUiIHhsaW5rOmhyZWY9IiNsaW5lYXJHcmFkaWVudDIyNDAiIHkxPSIzNS43NSIgeTI9IjQyLjUiLz48L2RlZnM+PGcgaWQ9ImxheWVyMSI+PHBhdGggZD0iTSA0MC40ODE4NjMgMzYuNDIxMTI3IEEgMTUuNjQ0NzM3IDguMzk2ODkzNSAwIDEgMSAgOS4xOTIzODg1LDM2LjQyMTEyNyBBIDE1LjY0NDczNyA4LjM5Njg5MzUgMCAxIDEgIDQwLjQ4MTg2MyAzNi40MjExMjcgeiIgaWQ9InBhdGg4NjYwIiBzdHlsZT0iZmlsdGVyOmludmVydCgxMDAlKTtvcGFjaXR5OjAuMTQxMTc2NDc7Y29sb3I6IzAwMDAwMDtmaWxsOnVybCgjcmFkaWFsR3JhZGllbnQ4NjY4KTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTttYXJrZXItc3RhcnQ6bm9uZTttYXJrZXItbWlkOm5vbmU7bWFya2VyLWVuZDpub25lO3N0cm9rZS1taXRlcmxpbWl0OjEwO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGUiIHRyYW5zZm9ybT0ibWF0cml4KDEuNDg5NzM2LDAuMDAwMDAwLDAuMDAwMDAwLC0xLjAwMTI1MiwtMTIuNjQ3MTYsNzUuMzEyNjApIi8+PHBhdGggZD0iTSAzOC4zNzQ3Niw0NS4wMzQzNjkgQyAtMS42NTEwNDg2LDQ2LjM1NTUwOSA0LjY3NDc5NTQsMTIuMjkzNTUgMjUuNDk0NzksMTIuNDk3NjUgTCAyNS40OTQ3OSwzLjEyMjIzOTYgTCA0Mi4xNDMyNzEsMTcuNzA4ODE5IEwgMjUuNDk0NzksMzMuMDA2MzQ5IEMgMjUuNDk0NzksMzMuMDA2MzQ5IDI1LjQ5NDc5LDIzLjMzNzk2OSAyNS40OTQ3OSwyMy4zMzc5NjkgQyAxMS40MzE2OCwyMi43NTE5OTkgNy4zMTcyNjE0LDQ0Ljc3MDU0OSAzOC4zNzQ3Niw0NS4wMzQzNjkgeiAiIGlkPSJwYXRoMTQzMiIgc3R5bGU9ImZpbHRlcjppbnZlcnQoMTAwJSk7b3BhY2l0eToxO2NvbG9yOiMwMDAwMDA7ZmlsbDp1cmwoI2xpbmVhckdyYWRpZW50MjI0Nik7ZmlsbC1vcGFjaXR5OjEuMDtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6dXJsKCNsaW5lYXJHcmFkaWVudDIyMzgpO3N0cm9rZS13aWR0aDoxLjAwMDAwMDEyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO21hcmtlcjpub25lO21hcmtlci1zdGFydDpub25lO21hcmtlci1taWQ6bm9uZTttYXJrZXItZW5kOm5vbmU7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmJsb2NrO292ZXJmbG93OnZpc2libGUiLz48cGF0aCBkPSJNIDE2LjkyNDkyLDM5LjMxNTUxOSBDIDUuMjAxODIwNCwzMy4yMzU4OTIgOC43MzcxMjc0LDEzLjA4NzQ4OSAyNi41MDg1LDEzLjU0OTk1OSBMIDI2LjUwODUsNS40NTA4Njc4IEMgMjYuNTA4NSw1LjQ1MDg2NzggNDAuNTU2MjM4LDE3LjcxNDU4OSA0MC41NTYyMzgsMTcuNzE0NTg5IEwgMjYuNTA4NSwzMC42NTg2MTcgQyAyNi41MDg1LDMwLjY1ODYxNyAyNi41MDg1LDIyLjM4MDk3OSAyNi41MDg1LDIyLjM4MDk3OSBDIDExLjY2ODY1LDIyLjAzMjcwOSAxMi4zNDg1OSwzNS4xMzg1NzkgMTYuOTI0OTIsMzkuMzE1NTE5IHogIiBpZD0icGF0aDIxNzciIHN0eWxlPSJmaWx0ZXI6aW52ZXJ0KDEwMCUpO29wYWNpdHk6MC42OTg4NjM2MTtjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6dXJsKCNsaW5lYXJHcmFkaWVudDE3NjQpO3N0cm9rZS13aWR0aDowLjk5OTk5OTc7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7bWFya2VyLXN0YXJ0Om5vbmU7bWFya2VyLW1pZDpub25lO21hcmtlci1lbmQ6bm9uZTtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmJsb2NrO292ZXJmbG93OnZpc2libGUiLz48cGF0aCBkPSJNIDI2LjAzNjk4OSw0LjU2ODYwOTUgTCAzNi43MjM3MjcsMTQuNzk4MjQxIEMgMjkuNzg2MjI3LDE0Ljc5ODI0IDMyLjAzNjk4OSwyMy43MzU0MjQgMjUuOTExOTg5LDI2LjYxMDQyNCBMIDI1Ljk3NDQ4OSwyMi45NDM2MDkgQyAxMC43ODY5ODksMjIuODgxMTA5IDExLjY2MTk4OSwzOC40NDM2MDkgMjIuNzI0NDg5LDQyLjY5MzYwOSBDIDMuNjM2MzQxNCwzNy44MTE2ODEgNi4yODY5OTA0LDEzLjM4MTEwOSAyNS45MTE5ODksMTIuODgxMTEgTCAyNi4wMzY5ODksNC41Njg2MDk1IHogIiBpZD0icGF0aDQ5ODkiIHN0eWxlPSJmaWx0ZXI6aW52ZXJ0KDEwMCUpO29wYWNpdHk6MC40OTQzMTgxNztjb2xvcjojMDAwMDAwO2ZpbGw6dXJsKCNyYWRpYWxHcmFkaWVudDQ5OTcpO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDowLjk5OTk5OTc7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7bWFya2VyLXN0YXJ0Om5vbmU7bWFya2VyLW1pZDpub25lO21hcmtlci1lbmQ6bm9uZTtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlIi8+PC9nPjwvc3ZnPg=="); +} \ No newline at end of file diff --git a/resources/darkmode.css b/resources/darkmode.css index 4947452..f9a4884 100644 --- a/resources/darkmode.css +++ b/resources/darkmode.css @@ -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); diff --git a/resources/dmmd-preview.css b/resources/dmmd-preview.css index 7a7fd1d..c768a0e 100644 --- a/resources/dmmd-preview.css +++ b/resources/dmmd-preview.css @@ -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 { diff --git a/resources/pagedown_widget.css b/resources/pagedown_widget.css index f20c5e2..3601240 100644 --- a/resources/pagedown_widget.css +++ b/resources/pagedown_widget.css @@ -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 { diff --git a/resources/pagedown_widget.scss b/resources/pagedown_widget.scss index b6d2333..fa97abe 100644 --- a/resources/pagedown_widget.scss +++ b/resources/pagedown_widget.scss @@ -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); } } \ No newline at end of file diff --git a/resources/widgets.scss b/resources/widgets.scss index 3f2ea95..42e8c8e 100644 --- a/resources/widgets.scss +++ b/resources/widgets.scss @@ -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; } } diff --git a/resources/wpadmin/css/wpadmin.site.css b/resources/wpadmin/css/wpadmin.site.css index d60064c..d971f7c 100644 --- a/resources/wpadmin/css/wpadmin.site.css +++ b/resources/wpadmin/css/wpadmin.site.css @@ -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 { diff --git a/templates/contest/contests_summary.html b/templates/contest/contests_summary.html new file mode 100644 index 0000000..a7fbe12 --- /dev/null +++ b/templates/contest/contests_summary.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block title_row %}{% endblock %} +{% block title_ruler %}{% endblock %} + +{% block media %} + +{% endblock %} + +{% block body %} + + + + + + {% for contest in contests %} + + {% endfor %} + + + + + {% for rank, item in total_rank %} + + + + {% for point_contest, rank_contest in item.point_contests %} + + {% endfor %} + + + {% endfor %} + +
{{_('Rank')}}{{_('Name')}}{{ loop.index }}{{_('Points')}}
+ {{ rank }} + +
+ {{item.user.username}} +
+
{{item.user.first_name}}
+
+
{{ point_contest }}
+ {% if rank_contest %} +
(#{{ rank_contest }})
+ {% endif %} +
+ {{ item.points }} +
+{% endblock %} diff --git a/templates/contest/ranking-table.html b/templates/contest/ranking-table.html index e182717..18d64dd 100644 --- a/templates/contest/ranking-table.html +++ b/templates/contest/ranking-table.html @@ -13,14 +13,14 @@ {% endblock %} {% block user_footer %} - {% if user.user.first_name %} + {% if user.first_name %} {% endif %} - {% if user.user.last_name %} + {% if user.last_name %} {% endif %} {% endblock %} diff --git a/templates/feed/has_next.html b/templates/feed/has_next.html index 9cc45c9..f26caea 100644 --- a/templates/feed/has_next.html +++ b/templates/feed/has_next.html @@ -1,4 +1,4 @@ {% if has_next_page %} - + {% endif %} \ No newline at end of file diff --git a/templates/notification/list.html b/templates/notification/list.html index cd50e32..6aa7351 100644 --- a/templates/notification/list.html +++ b/templates/notification/list.html @@ -1,11 +1,8 @@ {% extends "base.html" %} {% block body %} - {% if not has_notifications %} -

{{ _('You have no notifications') }}

- {% else %} @@ -17,24 +14,15 @@ {% for notification in notifications %}
- {% if notification.comment %} - {{ link_user(notification.comment.author) }} - {% else %} - {{ link_user(notification.author) }} - {% endif %} - + {{ link_user(notification.author) }} {{ notification.category }} - {% if notification.comment %} - {{ notification.comment.page_title }} - {% else %} - {% autoescape off %} - {{notification.html_link}} - {% endautoescape %} - {% endif %} + {% autoescape off %} + {{notification.html_link}} + {% endautoescape %} {{ relative_time(notification.time) }} @@ -43,8 +31,5 @@ {% endfor %}
{% endif %} - {% endblock %} - \ No newline at end of file diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index 722f6da..f49506d 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -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(); ($('
').attr('action', window.location.pathname + '?' + form_serialize()) .append($('').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken') diff --git a/templates/problem/related_problems.html b/templates/problem/related_problems.html index 95ed05f..a92a75b 100644 --- a/templates/problem/related_problems.html +++ b/templates/problem/related_problems.html @@ -1,4 +1,4 @@ -{% if related_problems %} +{% if request.user.is_authenticated and related_problems %}

{{_('Recommended problems')}}:

    diff --git a/templates/problem/search-form.html b/templates/problem/search-form.html index 24b4600..53c9e84 100644 --- a/templates/problem/search-form.html +++ b/templates/problem/search-form.html @@ -34,13 +34,6 @@
{% endif %} - {% if has_have_editorial_option %} -
- - -
- {% endif %} {% if organizations %}
diff --git a/templates/submission/row.html b/templates/submission/row.html index 1c4d8d7..73523f6 100644 --- a/templates/submission/row.html +++ b/templates/submission/row.html @@ -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) %}
{%- if submission.is_graded -%} diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html index b03248a..d060ede 100644 --- a/templates/submission/user-ajax.html +++ b/templates/submission/user-ajax.html @@ -1,5 +1,5 @@

- {{_('Contest submissions of')}} {{link_user(profile)}} # + {{_('Contest submissions of')}} {{link_user(profile)}} #


{% if best_subtasks and subtasks %} @@ -19,7 +19,7 @@ {% 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 %} → {{ cur_subtask.submission.id }} @@ -43,7 +43,7 @@ {% for submission in submissions %} - {% 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) %} {% endblock %}