Merge pull request #61 from zhaospei/lms

add resource course views
This commit is contained in:
Tuan-Dung Bui 2023-03-01 20:42:41 +07:00 committed by GitHub
commit 0ee89f1c36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
281 changed files with 15287 additions and 13660 deletions

13
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,13 @@
repos:
- repo: https://github.com/rtts/djhtml
rev: 'v1.5.2' # replace with the latest tag on GitHub
hooks:
- id: djhtml
entry: djhtml -i -t 2
files: templates/.
- id: djcss
types: [scss]
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black

View file

@ -90,6 +90,8 @@ celery -A dmoj_celery worker
node websocket/daemon.js node websocket/daemon.js
``` ```
7. To use subdomain for each organization, go to admin page -> navigation bar -> sites, add domain name (e.g, "localhost:8000"). Then go to add `USE_SUBDOMAIN = True` to local_settings.py.
## Deploy ## Deploy
Most of the steps are similar to Django tutorials. Here are two usual steps: Most of the steps are similar to Django tutorials. Here are two usual steps:

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.18 on 2023-02-18 21:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0010_auto_20221028_0300"),
]
operations = [
migrations.AlterField(
model_name="message",
name="hidden",
field=models.BooleanField(
db_index=True, default=False, verbose_name="is hidden"
),
),
]

View file

@ -31,7 +31,7 @@ class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192) body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False) hidden = models.BooleanField(verbose_name="is hidden", default=False, db_index=True)
room = models.ForeignKey( room = models.ForeignKey(
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
) )

View file

@ -48,16 +48,26 @@ class ChatView(ListView):
super().__init__() super().__init__()
self.room_id = None self.room_id = None
self.room = None self.room = None
self.paginate_by = 50
self.messages = None self.messages = None
self.paginator = None self.page_size = 20
def get_queryset(self): def get_queryset(self):
return self.messages return self.messages
def has_next(self):
try:
msg = Message.objects.filter(room=self.room_id).earliest("id")
except Exception as e:
return False
return msg not in self.messages
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request_room = kwargs["room_id"] request_room = kwargs["room_id"]
page = request.GET.get("page") try:
last_id = int(request.GET.get("last_id"))
except Exception:
last_id = 1e15
only_messages = request.GET.get("only_messages")
if request_room: if request_room:
try: try:
@ -69,23 +79,20 @@ class ChatView(ListView):
else: else:
request_room = None request_room = None
if request_room != self.room_id or not self.messages:
self.room_id = request_room self.room_id = request_room
self.messages = Message.objects.filter(hidden=False, room=self.room_id) self.messages = Message.objects.filter(
self.paginator = Paginator(self.messages, self.paginate_by) hidden=False, room=self.room_id, id__lt=last_id
)[: self.page_size]
if page == None: if not only_messages:
update_last_seen(request, **kwargs) update_last_seen(request, **kwargs)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
cur_page = self.paginator.get_page(page)
return render( return render(
request, request,
"chat/message_list.html", "chat/message_list.html",
{ {
"object_list": cur_page.object_list, "object_list": self.messages,
"num_pages": self.paginator.num_pages, "has_next": self.has_next(),
}, },
) )
@ -96,6 +103,7 @@ class ChatView(ListView):
context["last_msg"] = event.last() context["last_msg"] = event.last()
context["status_sections"] = get_status_context(self.request) context["status_sections"] = get_status_context(self.request)
context["room"] = self.room_id context["room"] = self.room_id
context["has_next"] = self.has_next()
context["unread_count_lobby"] = get_unread_count(None, self.request.profile) context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
if self.room: if self.room:
users_room = [self.room.user_one, self.room.user_two] users_room = [self.room.user_one, self.room.user_two]

View file

@ -247,6 +247,7 @@ INSTALLED_APPS += (
) )
MIDDLEWARE = ( MIDDLEWARE = (
"judge.middleware.SlowRequestMiddleware",
"judge.middleware.ShortCircuitMiddleware", "judge.middleware.ShortCircuitMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
@ -262,6 +263,7 @@ MIDDLEWARE = (
"judge.middleware.DMOJImpersonationMiddleware", "judge.middleware.DMOJImpersonationMiddleware",
"judge.middleware.ContestMiddleware", "judge.middleware.ContestMiddleware",
"judge.middleware.DarkModeMiddleware", "judge.middleware.DarkModeMiddleware",
"judge.middleware.SubdomainMiddleware",
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
"judge.social_auth.SocialAuthExceptionMiddleware", "judge.social_auth.SocialAuthExceptionMiddleware",
"django.contrib.redirects.middleware.RedirectFallbackMiddleware", "django.contrib.redirects.middleware.RedirectFallbackMiddleware",
@ -468,6 +470,9 @@ MESSAGES_TO_LOAD = 15
ML_OUTPUT_PATH = None ML_OUTPUT_PATH = None
# Use subdomain for organizations
USE_SUBDOMAIN = False
try: try:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
exec(f.read(), globals()) exec(f.read(), globals())

View file

@ -64,6 +64,7 @@ from judge.views import (
widgets, widgets,
internal, internal,
resolver, resolver,
course,
) )
from judge.views.problem_data import ( from judge.views.problem_data import (
ProblemDataView, ProblemDataView,
@ -230,18 +231,19 @@ urlpatterns = [
url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")), url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")),
url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"), url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"),
url( url(
r"^problems/feed/", r"^problems/feed/$",
paged_list_view(problem.ProblemFeed, "problem_feed", feed_type="for_you"), problem.ProblemFeed.as_view(feed_type="for_you"),
name="problem_feed",
), ),
url( url(
r"^problems/feed/new/", r"^problems/feed/new/$",
paged_list_view(problem.ProblemFeed, "problem_feed_new", feed_type="new"), problem.ProblemFeed.as_view(feed_type="new"),
name="problem_feed_new",
), ),
url( url(
r"^problems/feed/volunteer/", r"^problems/feed/volunteer/$",
paged_list_view( problem.ProblemFeed.as_view(feed_type="volunteer"),
problem.ProblemFeed, "problem_feed_volunteer", feed_type="volunteer" name="problem_feed_volunteer",
),
), ),
url( url(
r"^problem/(?P<problem>[^/]+)", r"^problem/(?P<problem>[^/]+)",
@ -368,6 +370,10 @@ urlpatterns = [
r"^submissions/user/(?P<user>\w+)/", r"^submissions/user/(?P<user>\w+)/",
paged_list_view(submission.AllUserSubmissions, "all_user_submissions"), paged_list_view(submission.AllUserSubmissions, "all_user_submissions"),
), ),
url(
r"^submissions/friends/",
paged_list_view(submission.AllFriendSubmissions, "all_friend_submissions"),
),
url( url(
r"^src/(?P<submission>\d+)/raw$", r"^src/(?P<submission>\d+)/raw$",
submission.SubmissionSourceRaw.as_view(), submission.SubmissionSourceRaw.as_view(),
@ -486,6 +492,39 @@ urlpatterns = [
), ),
), ),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
url(
r"^courses/(?P<pk>\d+)-(?P<slug>[\w-]*)",
include(
[
url(
r"^$",
course.CourseHome.as_view(),
name="course_home",
),
url(
r"^/resource/$",
course.CourseResourceList.as_view(),
name="course_resource",
),
url(
r"^/resource_edit/$",
course.CourseResourceEdit.as_view(),
name="course_resource_edit",
),
url(
r"^/resource/(?P<pk>\d+)/$",
course.CourseResouceDetail.as_view(),
name="course_resource_detail",
),
url(
r"^/resource/(?P<pk>\d+)/edit",
course.CourseResourceDetailEdit.as_view(),
name="course_resource_detail_edit",
),
]
),
),
url( url(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$", r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
contests.ContestCalendar.as_view(), contests.ContestCalendar.as_view(),
@ -536,13 +575,6 @@ urlpatterns = [
url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"), url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"),
url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"), url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"),
url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"), url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"),
url(
r"^/rank/(?P<problem>\w+)/",
paged_list_view(
ranked_submission.ContestRankedSubmission,
"contest_ranked_submissions",
),
),
url( url(
r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)", r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)",
paged_list_view( paged_list_view(
@ -751,7 +783,7 @@ urlpatterns = [
] ]
), ),
), ),
url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")), url(r"^blog/", blog.PostList.as_view(), name="blog_post_list"),
url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"), url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"),
url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"), url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"),
url( url(

View file

@ -39,8 +39,11 @@ from judge.models import (
Submission, Submission,
Ticket, Ticket,
VolunteerProblemVote, VolunteerProblemVote,
Course,
) )
from judge.models.course import CourseResource
admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(BlogPost, BlogPostAdmin)
admin.site.register(Comment, CommentAdmin) admin.site.register(Comment, CommentAdmin)
@ -64,3 +67,5 @@ admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin) admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin) admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course)
admin.site.register(CourseResource)

View file

@ -22,11 +22,23 @@ class CommentForm(ModelForm):
class CommentAdmin(VersionAdmin): class CommentAdmin(VersionAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("author", "page", "parent", "score", "hidden")}), (
None,
{
"fields": (
"author",
"parent",
"score",
"hidden",
"content_type",
"object_id",
)
},
),
("Content", {"fields": ("body",)}), ("Content", {"fields": ("body",)}),
) )
list_display = ["author", "linked_page", "time"] list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "page", "body"] search_fields = ["author__user__username", "body"]
readonly_fields = ["score"] readonly_fields = ["score"]
actions = ["hide_comment", "unhide_comment"] actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"] list_filter = ["hidden"]
@ -66,16 +78,6 @@ class CommentAdmin(VersionAdmin):
unhide_comment.short_description = _("Unhide comments") unhide_comment.short_description = _("Unhide comments")
def linked_page(self, obj):
link = obj.link
if link is not None:
return format_html('<a href="{0}">{1}</a>', link, obj.page)
else:
return format_html("{0}", obj.page)
linked_page.short_description = _("Associated page")
linked_page.admin_order_field = "page"
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(CommentAdmin, self).save_model(request, obj, form, change) super(CommentAdmin, self).save_model(request, obj, form, change)
if obj.hidden: if obj.hidden:

View file

@ -365,22 +365,27 @@ class ProblemAdmin(CompareVersionAdmin):
obj.is_organization_private = obj.organizations.count() > 0 obj.is_organization_private = obj.organizations.count() > 0
obj.save() obj.save()
# Create notification # Create notification
if "is_public" in form.changed_data: if "is_public" in form.changed_data or "organizations" in form.changed_data:
users = set(obj.authors.all()) users = set(obj.authors.all())
users = users.union(users, set(obj.curators.all())) users = users.union(users, set(obj.curators.all()))
orgs = []
if obj.organizations.count() > 0: if obj.organizations.count() > 0:
for org in obj.organizations.all(): for org in obj.organizations.all():
users = users.union(users, set(org.admins.all())) users = users.union(users, set(org.admins.all()))
orgs.append(org.name)
else: else:
admins = Profile.objects.filter(user__is_superuser=True).all() admins = Profile.objects.filter(user__is_superuser=True).all()
users = users.union(users, admins) users = users.union(users, admins)
link = reverse_lazy("admin:judge_problem_change", args=(obj.id,)) link = reverse_lazy("admin:judge_problem_change", args=(obj.id,))
html = f'<a href="{link}">{obj.name}</a>' html = f'<a href="{link}">{obj.name}</a>'
category = "Problem public: " + str(obj.is_public)
if orgs:
category += " (" + ", ".join(orgs) + ")"
for user in users: for user in users:
notification = Notification( notification = Notification(
owner=user, owner=user,
html_link=html, html_link=html,
category="Problem public: " + str(obj.is_public), category=category,
author=request.profile, author=request.profile,
) )
notification.save() notification.save()

View file

@ -22,7 +22,7 @@ from reversion import revisions
from reversion.models import Revision, Version from reversion.models import Revision, Version
from judge.dblock import LockModel from judge.dblock import LockModel
from judge.models import Comment, CommentLock, Notification from judge.models import Comment, Notification
from judge.widgets import HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text from judge.jinja2.reference import get_user_from_text
@ -90,7 +90,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def is_comment_locked(self): def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"): if self.request.user.has_perm("judge.override_comment_lock"):
return False return False
return CommentLock.objects.filter(page=self.get_comment_page()).exists() or ( return (
self.request.in_contest self.request.in_contest
and self.request.participation.contest.use_clarifications and self.request.participation.contest.use_clarifications
) )
@ -98,8 +98,6 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
@method_decorator(login_required) @method_decorator(login_required)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
page = self.get_comment_page()
if self.is_comment_locked(): if self.is_comment_locked():
return HttpResponseForbidden() return HttpResponseForbidden()
@ -110,16 +108,14 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
except ValueError: except ValueError:
return HttpResponseNotFound() return HttpResponseNotFound()
else: else:
if not Comment.objects.filter( if not self.object.comments.filter(hidden=False, id=parent).exists():
hidden=False, id=parent, page=page
).exists():
return HttpResponseNotFound() return HttpResponseNotFound()
form = CommentForm(request, request.POST) form = CommentForm(request, request.POST)
if form.is_valid(): if form.is_valid():
comment = form.save(commit=False) comment = form.save(commit=False)
comment.author = request.profile comment.author = request.profile
comment.page = page comment.linked_object = self.object
with LockModel( with LockModel(
write=(Comment, Revision, Version), read=(ContentType,) write=(Comment, Revision, Version), read=(ContentType,)
@ -136,7 +132,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
notification_reply.save() notification_reply.save()
# add notification for page authors # add notification for page authors
page_authors = comment.page_object.authors.all() page_authors = comment.linked_object.authors.all()
for user in page_authors: for user in page_authors:
if user == comment.author: if user == comment.author:
continue continue
@ -149,7 +145,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
add_mention_notifications(comment) add_mention_notifications(comment)
return HttpResponseRedirect(request.path) return HttpResponseRedirect(comment.get_absolute_url())
context = self.get_context_data(object=self.object, comment_form=form) context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context) return self.render_to_response(context)
@ -159,15 +155,13 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
return self.render_to_response( return self.render_to_response(
self.get_context_data( self.get_context_data(
object=self.object, object=self.object,
comment_form=CommentForm( comment_form=CommentForm(request, initial={"parent": None}),
request, initial={"page": self.get_comment_page(), "parent": None}
),
) )
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs) context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page()) queryset = self.object.comments
context["has_comments"] = queryset.exists() context["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked() context["comment_lock"] = self.is_comment_locked()
queryset = ( queryset = (

View file

@ -263,6 +263,7 @@ class EditOrganizationContestForm(ModelForm):
"curators", "curators",
"testers", "testers",
"time_limit", "time_limit",
"freeze_after",
"use_clarifications", "use_clarifications",
"hide_problem_tags", "hide_problem_tags",
"scoreboard_visibility", "scoreboard_visibility",

View file

@ -98,7 +98,7 @@ def markdown(value, lazy_load=False):
if not html: if not html:
html = escape(value) html = escape(value)
if lazy_load or True: if lazy_load:
soup = BeautifulSoup(html, features="html.parser") soup = BeautifulSoup(html, features="html.parser")
for img in soup.findAll("img"): for img in soup.findAll("img"):
if img.get("src"): if img.get("src"):

View file

@ -3,34 +3,39 @@ from . import registry
@registry.function @registry.function
def submission_layout( def submission_layout(
submission, profile_id, user, editable_problem_ids, completed_problem_ids submission,
profile_id,
user,
editable_problem_ids,
completed_problem_ids,
tester_problem_ids,
): ):
problem_id = submission.problem_id problem_id = submission.problem_id
can_view = False
if problem_id in editable_problem_ids: if problem_id in editable_problem_ids:
can_view = True return True
if problem_id in tester_problem_ids:
return True
if profile_id == submission.user_id: if profile_id == submission.user_id:
can_view = True return True
if user.has_perm("judge.change_submission"): if user.has_perm("judge.change_submission"):
can_view = True return True
if user.has_perm("judge.view_all_submission"): if user.has_perm("judge.view_all_submission"):
can_view = True return True
if submission.problem.is_public and user.has_perm("judge.view_public_submission"): if submission.problem.is_public and user.has_perm("judge.view_public_submission"):
can_view = True return True
if submission.problem_id in completed_problem_ids: if hasattr(submission, "contest"):
can_view |= (
submission.problem.is_public or profile_id in submission.problem.tester_ids
)
if not can_view and hasattr(submission, "contest"):
contest = submission.contest.participation.contest contest = submission.contest.participation.contest
if contest.is_editable_by(user): if contest.is_editable_by(user):
can_view = True return True
return can_view if submission.problem_id in completed_problem_ids and submission.problem.is_public:
return True
return False

View file

@ -1,7 +1,19 @@
import time
import logging
from django.conf import settings from django.conf import settings
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, Http404
from django.urls import Resolver404, resolve, reverse from django.urls import Resolver404, resolve, reverse
from django.utils.http import urlquote from django.utils.http import urlquote
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext as _
from judge.models import Organization
from judge.utils.views import generic_message
USED_DOMAINS = ["www"]
class ShortCircuitMiddleware: class ShortCircuitMiddleware:
@ -82,3 +94,71 @@ class DarkModeMiddleware(object):
reverse("toggle_darkmode") + "?next=" + urlquote(request.path) reverse("toggle_darkmode") + "?next=" + urlquote(request.path)
) )
return self.get_response(request) return self.get_response(request)
class SubdomainMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.organization = None
if not settings.USE_SUBDOMAIN:
return self.get_response(request)
domain = request.get_host()
site = get_current_site(request).domain
subdomain = domain[: len(domain) - len(site)].lower()
if len(subdomain) <= 1:
return self.get_response(request)
subdomain = subdomain[:-1]
if subdomain in USED_DOMAINS:
return self.get_response(request)
try:
organization = Organization.objects.get(slug=subdomain)
if request.profile and organization in request.profile.organizations.all():
request.organization = organization
else:
if request.profile:
return generic_message(
request,
_("No permission"),
_("You need to join this group first"),
status=404,
)
if not request.GET.get("next", None):
return HttpResponseRedirect(
reverse("auth_login") + "?next=" + urlquote(request.path)
)
except ObjectDoesNotExist:
return generic_message(
request,
_("No such group"),
_("No such group"),
status=404,
)
return self.get_response(request)
class SlowRequestMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
logger = logging.getLogger("judge.slow_request")
start_time = time.time()
response = self.get_response(request)
response_time = time.time() - start_time
if response_time > 9:
message = {
"message": "Slow request",
"url": request.build_absolute_uri(),
"response_time": response_time * 1000,
"method": request.method,
"profile": request.profile,
}
logger.info(message)
return response

View file

@ -0,0 +1,36 @@
# Generated by Django 3.2.16 on 2023-01-23 23:39
from django.db import migrations, models
def make_slug_unique(apps, schema_editor):
Organization = apps.get_model("judge", "Organization")
slugs = Organization.objects.values_list("slug", flat=True)
slugs = set([i.lower() for i in slugs])
for slug in slugs:
orgs = Organization.objects.filter(slug=slug)
if len(orgs) > 1:
for org in orgs:
org.slug += "-" + str(org.id)
org.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0144_auto_20230103_0523"),
]
operations = [
migrations.RunPython(make_slug_unique, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name="organization",
name="slug",
field=models.SlugField(
help_text="Organization name shown in URL",
max_length=128,
unique=True,
verbose_name="organization slug",
),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 3.2.16 on 2023-01-25 19:12
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0145_alter_organization_slug"),
]
operations = [
migrations.AlterField(
model_name="organization",
name="slug",
field=models.SlugField(
help_text="Organization name shown in URL",
max_length=128,
unique=True,
validators=[
django.core.validators.RegexValidator(
"^[-a-zA-Z0-9]+$", "Only alphanumeric and hyphens"
)
],
verbose_name="organization slug",
),
),
]

View file

@ -0,0 +1,678 @@
# Generated by Django 3.2.16 on 2023-01-30 11:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0146_alter_organization_slug"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="timezone",
field=models.CharField(
choices=[
(
"Africa",
[
("Africa/Abidjan", "Abidjan"),
("Africa/Accra", "Accra"),
("Africa/Addis_Ababa", "Addis_Ababa"),
("Africa/Algiers", "Algiers"),
("Africa/Asmara", "Asmara"),
("Africa/Asmera", "Asmera"),
("Africa/Bamako", "Bamako"),
("Africa/Bangui", "Bangui"),
("Africa/Banjul", "Banjul"),
("Africa/Bissau", "Bissau"),
("Africa/Blantyre", "Blantyre"),
("Africa/Brazzaville", "Brazzaville"),
("Africa/Bujumbura", "Bujumbura"),
("Africa/Cairo", "Cairo"),
("Africa/Casablanca", "Casablanca"),
("Africa/Ceuta", "Ceuta"),
("Africa/Conakry", "Conakry"),
("Africa/Dakar", "Dakar"),
("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
("Africa/Djibouti", "Djibouti"),
("Africa/Douala", "Douala"),
("Africa/El_Aaiun", "El_Aaiun"),
("Africa/Freetown", "Freetown"),
("Africa/Gaborone", "Gaborone"),
("Africa/Harare", "Harare"),
("Africa/Johannesburg", "Johannesburg"),
("Africa/Juba", "Juba"),
("Africa/Kampala", "Kampala"),
("Africa/Khartoum", "Khartoum"),
("Africa/Kigali", "Kigali"),
("Africa/Kinshasa", "Kinshasa"),
("Africa/Lagos", "Lagos"),
("Africa/Libreville", "Libreville"),
("Africa/Lome", "Lome"),
("Africa/Luanda", "Luanda"),
("Africa/Lubumbashi", "Lubumbashi"),
("Africa/Lusaka", "Lusaka"),
("Africa/Malabo", "Malabo"),
("Africa/Maputo", "Maputo"),
("Africa/Maseru", "Maseru"),
("Africa/Mbabane", "Mbabane"),
("Africa/Mogadishu", "Mogadishu"),
("Africa/Monrovia", "Monrovia"),
("Africa/Nairobi", "Nairobi"),
("Africa/Ndjamena", "Ndjamena"),
("Africa/Niamey", "Niamey"),
("Africa/Nouakchott", "Nouakchott"),
("Africa/Ouagadougou", "Ouagadougou"),
("Africa/Porto-Novo", "Porto-Novo"),
("Africa/Sao_Tome", "Sao_Tome"),
("Africa/Timbuktu", "Timbuktu"),
("Africa/Tripoli", "Tripoli"),
("Africa/Tunis", "Tunis"),
("Africa/Windhoek", "Windhoek"),
],
),
(
"America",
[
("America/Adak", "Adak"),
("America/Anchorage", "Anchorage"),
("America/Anguilla", "Anguilla"),
("America/Antigua", "Antigua"),
("America/Araguaina", "Araguaina"),
(
"America/Argentina/Buenos_Aires",
"Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "Argentina/Cordoba"),
("America/Argentina/Jujuy", "Argentina/Jujuy"),
("America/Argentina/La_Rioja", "Argentina/La_Rioja"),
("America/Argentina/Mendoza", "Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "Argentina/Salta"),
("America/Argentina/San_Juan", "Argentina/San_Juan"),
("America/Argentina/San_Luis", "Argentina/San_Luis"),
("America/Argentina/Tucuman", "Argentina/Tucuman"),
("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
("America/Aruba", "Aruba"),
("America/Asuncion", "Asuncion"),
("America/Atikokan", "Atikokan"),
("America/Atka", "Atka"),
("America/Bahia", "Bahia"),
("America/Bahia_Banderas", "Bahia_Banderas"),
("America/Barbados", "Barbados"),
("America/Belem", "Belem"),
("America/Belize", "Belize"),
("America/Blanc-Sablon", "Blanc-Sablon"),
("America/Boa_Vista", "Boa_Vista"),
("America/Bogota", "Bogota"),
("America/Boise", "Boise"),
("America/Buenos_Aires", "Buenos_Aires"),
("America/Cambridge_Bay", "Cambridge_Bay"),
("America/Campo_Grande", "Campo_Grande"),
("America/Cancun", "Cancun"),
("America/Caracas", "Caracas"),
("America/Catamarca", "Catamarca"),
("America/Cayenne", "Cayenne"),
("America/Cayman", "Cayman"),
("America/Chicago", "Chicago"),
("America/Chihuahua", "Chihuahua"),
("America/Ciudad_Juarez", "Ciudad_Juarez"),
("America/Coral_Harbour", "Coral_Harbour"),
("America/Cordoba", "Cordoba"),
("America/Costa_Rica", "Costa_Rica"),
("America/Creston", "Creston"),
("America/Cuiaba", "Cuiaba"),
("America/Curacao", "Curacao"),
("America/Danmarkshavn", "Danmarkshavn"),
("America/Dawson", "Dawson"),
("America/Dawson_Creek", "Dawson_Creek"),
("America/Denver", "Denver"),
("America/Detroit", "Detroit"),
("America/Dominica", "Dominica"),
("America/Edmonton", "Edmonton"),
("America/Eirunepe", "Eirunepe"),
("America/El_Salvador", "El_Salvador"),
("America/Ensenada", "Ensenada"),
("America/Fort_Nelson", "Fort_Nelson"),
("America/Fort_Wayne", "Fort_Wayne"),
("America/Fortaleza", "Fortaleza"),
("America/Glace_Bay", "Glace_Bay"),
("America/Godthab", "Godthab"),
("America/Goose_Bay", "Goose_Bay"),
("America/Grand_Turk", "Grand_Turk"),
("America/Grenada", "Grenada"),
("America/Guadeloupe", "Guadeloupe"),
("America/Guatemala", "Guatemala"),
("America/Guayaquil", "Guayaquil"),
("America/Guyana", "Guyana"),
("America/Halifax", "Halifax"),
("America/Havana", "Havana"),
("America/Hermosillo", "Hermosillo"),
("America/Indiana/Indianapolis", "Indiana/Indianapolis"),
("America/Indiana/Knox", "Indiana/Knox"),
("America/Indiana/Marengo", "Indiana/Marengo"),
("America/Indiana/Petersburg", "Indiana/Petersburg"),
("America/Indiana/Tell_City", "Indiana/Tell_City"),
("America/Indiana/Vevay", "Indiana/Vevay"),
("America/Indiana/Vincennes", "Indiana/Vincennes"),
("America/Indiana/Winamac", "Indiana/Winamac"),
("America/Indianapolis", "Indianapolis"),
("America/Inuvik", "Inuvik"),
("America/Iqaluit", "Iqaluit"),
("America/Jamaica", "Jamaica"),
("America/Jujuy", "Jujuy"),
("America/Juneau", "Juneau"),
("America/Kentucky/Louisville", "Kentucky/Louisville"),
("America/Kentucky/Monticello", "Kentucky/Monticello"),
("America/Knox_IN", "Knox_IN"),
("America/Kralendijk", "Kralendijk"),
("America/La_Paz", "La_Paz"),
("America/Lima", "Lima"),
("America/Los_Angeles", "Los_Angeles"),
("America/Louisville", "Louisville"),
("America/Lower_Princes", "Lower_Princes"),
("America/Maceio", "Maceio"),
("America/Managua", "Managua"),
("America/Manaus", "Manaus"),
("America/Marigot", "Marigot"),
("America/Martinique", "Martinique"),
("America/Matamoros", "Matamoros"),
("America/Mazatlan", "Mazatlan"),
("America/Mendoza", "Mendoza"),
("America/Menominee", "Menominee"),
("America/Merida", "Merida"),
("America/Metlakatla", "Metlakatla"),
("America/Mexico_City", "Mexico_City"),
("America/Miquelon", "Miquelon"),
("America/Moncton", "Moncton"),
("America/Monterrey", "Monterrey"),
("America/Montevideo", "Montevideo"),
("America/Montreal", "Montreal"),
("America/Montserrat", "Montserrat"),
("America/Nassau", "Nassau"),
("America/New_York", "New_York"),
("America/Nipigon", "Nipigon"),
("America/Nome", "Nome"),
("America/Noronha", "Noronha"),
("America/North_Dakota/Beulah", "North_Dakota/Beulah"),
("America/North_Dakota/Center", "North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"North_Dakota/New_Salem",
),
("America/Nuuk", "Nuuk"),
("America/Ojinaga", "Ojinaga"),
("America/Panama", "Panama"),
("America/Pangnirtung", "Pangnirtung"),
("America/Paramaribo", "Paramaribo"),
("America/Phoenix", "Phoenix"),
("America/Port-au-Prince", "Port-au-Prince"),
("America/Port_of_Spain", "Port_of_Spain"),
("America/Porto_Acre", "Porto_Acre"),
("America/Porto_Velho", "Porto_Velho"),
("America/Puerto_Rico", "Puerto_Rico"),
("America/Punta_Arenas", "Punta_Arenas"),
("America/Rainy_River", "Rainy_River"),
("America/Rankin_Inlet", "Rankin_Inlet"),
("America/Recife", "Recife"),
("America/Regina", "Regina"),
("America/Resolute", "Resolute"),
("America/Rio_Branco", "Rio_Branco"),
("America/Rosario", "Rosario"),
("America/Santa_Isabel", "Santa_Isabel"),
("America/Santarem", "Santarem"),
("America/Santiago", "Santiago"),
("America/Santo_Domingo", "Santo_Domingo"),
("America/Sao_Paulo", "Sao_Paulo"),
("America/Scoresbysund", "Scoresbysund"),
("America/Shiprock", "Shiprock"),
("America/Sitka", "Sitka"),
("America/St_Barthelemy", "St_Barthelemy"),
("America/St_Johns", "St_Johns"),
("America/St_Kitts", "St_Kitts"),
("America/St_Lucia", "St_Lucia"),
("America/St_Thomas", "St_Thomas"),
("America/St_Vincent", "St_Vincent"),
("America/Swift_Current", "Swift_Current"),
("America/Tegucigalpa", "Tegucigalpa"),
("America/Thule", "Thule"),
("America/Thunder_Bay", "Thunder_Bay"),
("America/Tijuana", "Tijuana"),
("America/Toronto", "Toronto"),
("America/Tortola", "Tortola"),
("America/Vancouver", "Vancouver"),
("America/Virgin", "Virgin"),
("America/Whitehorse", "Whitehorse"),
("America/Winnipeg", "Winnipeg"),
("America/Yakutat", "Yakutat"),
("America/Yellowknife", "Yellowknife"),
],
),
(
"Antarctica",
[
("Antarctica/Casey", "Casey"),
("Antarctica/Davis", "Davis"),
("Antarctica/DumontDUrville", "DumontDUrville"),
("Antarctica/Macquarie", "Macquarie"),
("Antarctica/Mawson", "Mawson"),
("Antarctica/McMurdo", "McMurdo"),
("Antarctica/Palmer", "Palmer"),
("Antarctica/Rothera", "Rothera"),
("Antarctica/South_Pole", "South_Pole"),
("Antarctica/Syowa", "Syowa"),
("Antarctica/Troll", "Troll"),
("Antarctica/Vostok", "Vostok"),
],
),
("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
(
"Asia",
[
("Asia/Aden", "Aden"),
("Asia/Almaty", "Almaty"),
("Asia/Amman", "Amman"),
("Asia/Anadyr", "Anadyr"),
("Asia/Aqtau", "Aqtau"),
("Asia/Aqtobe", "Aqtobe"),
("Asia/Ashgabat", "Ashgabat"),
("Asia/Ashkhabad", "Ashkhabad"),
("Asia/Atyrau", "Atyrau"),
("Asia/Baghdad", "Baghdad"),
("Asia/Bahrain", "Bahrain"),
("Asia/Baku", "Baku"),
("Asia/Bangkok", "Bangkok"),
("Asia/Barnaul", "Barnaul"),
("Asia/Beirut", "Beirut"),
("Asia/Bishkek", "Bishkek"),
("Asia/Brunei", "Brunei"),
("Asia/Calcutta", "Calcutta"),
("Asia/Chita", "Chita"),
("Asia/Choibalsan", "Choibalsan"),
("Asia/Chongqing", "Chongqing"),
("Asia/Chungking", "Chungking"),
("Asia/Colombo", "Colombo"),
("Asia/Dacca", "Dacca"),
("Asia/Damascus", "Damascus"),
("Asia/Dhaka", "Dhaka"),
("Asia/Dili", "Dili"),
("Asia/Dubai", "Dubai"),
("Asia/Dushanbe", "Dushanbe"),
("Asia/Famagusta", "Famagusta"),
("Asia/Gaza", "Gaza"),
("Asia/Harbin", "Harbin"),
("Asia/Hebron", "Hebron"),
("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
("Asia/Hong_Kong", "Hong_Kong"),
("Asia/Hovd", "Hovd"),
("Asia/Irkutsk", "Irkutsk"),
("Asia/Istanbul", "Istanbul"),
("Asia/Jakarta", "Jakarta"),
("Asia/Jayapura", "Jayapura"),
("Asia/Jerusalem", "Jerusalem"),
("Asia/Kabul", "Kabul"),
("Asia/Kamchatka", "Kamchatka"),
("Asia/Karachi", "Karachi"),
("Asia/Kashgar", "Kashgar"),
("Asia/Kathmandu", "Kathmandu"),
("Asia/Katmandu", "Katmandu"),
("Asia/Khandyga", "Khandyga"),
("Asia/Kolkata", "Kolkata"),
("Asia/Krasnoyarsk", "Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
("Asia/Kuching", "Kuching"),
("Asia/Kuwait", "Kuwait"),
("Asia/Macao", "Macao"),
("Asia/Macau", "Macau"),
("Asia/Magadan", "Magadan"),
("Asia/Makassar", "Makassar"),
("Asia/Manila", "Manila"),
("Asia/Muscat", "Muscat"),
("Asia/Nicosia", "Nicosia"),
("Asia/Novokuznetsk", "Novokuznetsk"),
("Asia/Novosibirsk", "Novosibirsk"),
("Asia/Omsk", "Omsk"),
("Asia/Oral", "Oral"),
("Asia/Phnom_Penh", "Phnom_Penh"),
("Asia/Pontianak", "Pontianak"),
("Asia/Pyongyang", "Pyongyang"),
("Asia/Qatar", "Qatar"),
("Asia/Qostanay", "Qostanay"),
("Asia/Qyzylorda", "Qyzylorda"),
("Asia/Rangoon", "Rangoon"),
("Asia/Riyadh", "Riyadh"),
("Asia/Saigon", "Saigon"),
("Asia/Sakhalin", "Sakhalin"),
("Asia/Samarkand", "Samarkand"),
("Asia/Seoul", "Seoul"),
("Asia/Shanghai", "Shanghai"),
("Asia/Singapore", "Singapore"),
("Asia/Srednekolymsk", "Srednekolymsk"),
("Asia/Taipei", "Taipei"),
("Asia/Tashkent", "Tashkent"),
("Asia/Tbilisi", "Tbilisi"),
("Asia/Tehran", "Tehran"),
("Asia/Tel_Aviv", "Tel_Aviv"),
("Asia/Thimbu", "Thimbu"),
("Asia/Thimphu", "Thimphu"),
("Asia/Tokyo", "Tokyo"),
("Asia/Tomsk", "Tomsk"),
("Asia/Ujung_Pandang", "Ujung_Pandang"),
("Asia/Ulaanbaatar", "Ulaanbaatar"),
("Asia/Ulan_Bator", "Ulan_Bator"),
("Asia/Urumqi", "Urumqi"),
("Asia/Ust-Nera", "Ust-Nera"),
("Asia/Vientiane", "Vientiane"),
("Asia/Vladivostok", "Vladivostok"),
("Asia/Yakutsk", "Yakutsk"),
("Asia/Yangon", "Yangon"),
("Asia/Yekaterinburg", "Yekaterinburg"),
("Asia/Yerevan", "Yerevan"),
],
),
(
"Atlantic",
[
("Atlantic/Azores", "Azores"),
("Atlantic/Bermuda", "Bermuda"),
("Atlantic/Canary", "Canary"),
("Atlantic/Cape_Verde", "Cape_Verde"),
("Atlantic/Faeroe", "Faeroe"),
("Atlantic/Faroe", "Faroe"),
("Atlantic/Jan_Mayen", "Jan_Mayen"),
("Atlantic/Madeira", "Madeira"),
("Atlantic/Reykjavik", "Reykjavik"),
("Atlantic/South_Georgia", "South_Georgia"),
("Atlantic/St_Helena", "St_Helena"),
("Atlantic/Stanley", "Stanley"),
],
),
(
"Australia",
[
("Australia/ACT", "ACT"),
("Australia/Adelaide", "Adelaide"),
("Australia/Brisbane", "Brisbane"),
("Australia/Broken_Hill", "Broken_Hill"),
("Australia/Canberra", "Canberra"),
("Australia/Currie", "Currie"),
("Australia/Darwin", "Darwin"),
("Australia/Eucla", "Eucla"),
("Australia/Hobart", "Hobart"),
("Australia/LHI", "LHI"),
("Australia/Lindeman", "Lindeman"),
("Australia/Lord_Howe", "Lord_Howe"),
("Australia/Melbourne", "Melbourne"),
("Australia/NSW", "NSW"),
("Australia/North", "North"),
("Australia/Perth", "Perth"),
("Australia/Queensland", "Queensland"),
("Australia/South", "South"),
("Australia/Sydney", "Sydney"),
("Australia/Tasmania", "Tasmania"),
("Australia/Victoria", "Victoria"),
("Australia/West", "West"),
("Australia/Yancowinna", "Yancowinna"),
],
),
(
"Brazil",
[
("Brazil/Acre", "Acre"),
("Brazil/DeNoronha", "DeNoronha"),
("Brazil/East", "East"),
("Brazil/West", "West"),
],
),
(
"Canada",
[
("Canada/Atlantic", "Atlantic"),
("Canada/Central", "Central"),
("Canada/Eastern", "Eastern"),
("Canada/Mountain", "Mountain"),
("Canada/Newfoundland", "Newfoundland"),
("Canada/Pacific", "Pacific"),
("Canada/Saskatchewan", "Saskatchewan"),
("Canada/Yukon", "Yukon"),
],
),
(
"Chile",
[
("Chile/Continental", "Continental"),
("Chile/EasterIsland", "EasterIsland"),
],
),
(
"Etc",
[
("Etc/Greenwich", "Greenwich"),
("Etc/UCT", "UCT"),
("Etc/UTC", "UTC"),
("Etc/Universal", "Universal"),
("Etc/Zulu", "Zulu"),
],
),
(
"Europe",
[
("Europe/Amsterdam", "Amsterdam"),
("Europe/Andorra", "Andorra"),
("Europe/Astrakhan", "Astrakhan"),
("Europe/Athens", "Athens"),
("Europe/Belfast", "Belfast"),
("Europe/Belgrade", "Belgrade"),
("Europe/Berlin", "Berlin"),
("Europe/Bratislava", "Bratislava"),
("Europe/Brussels", "Brussels"),
("Europe/Bucharest", "Bucharest"),
("Europe/Budapest", "Budapest"),
("Europe/Busingen", "Busingen"),
("Europe/Chisinau", "Chisinau"),
("Europe/Copenhagen", "Copenhagen"),
("Europe/Dublin", "Dublin"),
("Europe/Gibraltar", "Gibraltar"),
("Europe/Guernsey", "Guernsey"),
("Europe/Helsinki", "Helsinki"),
("Europe/Isle_of_Man", "Isle_of_Man"),
("Europe/Istanbul", "Istanbul"),
("Europe/Jersey", "Jersey"),
("Europe/Kaliningrad", "Kaliningrad"),
("Europe/Kiev", "Kiev"),
("Europe/Kirov", "Kirov"),
("Europe/Kyiv", "Kyiv"),
("Europe/Lisbon", "Lisbon"),
("Europe/Ljubljana", "Ljubljana"),
("Europe/London", "London"),
("Europe/Luxembourg", "Luxembourg"),
("Europe/Madrid", "Madrid"),
("Europe/Malta", "Malta"),
("Europe/Mariehamn", "Mariehamn"),
("Europe/Minsk", "Minsk"),
("Europe/Monaco", "Monaco"),
("Europe/Moscow", "Moscow"),
("Europe/Nicosia", "Nicosia"),
("Europe/Oslo", "Oslo"),
("Europe/Paris", "Paris"),
("Europe/Podgorica", "Podgorica"),
("Europe/Prague", "Prague"),
("Europe/Riga", "Riga"),
("Europe/Rome", "Rome"),
("Europe/Samara", "Samara"),
("Europe/San_Marino", "San_Marino"),
("Europe/Sarajevo", "Sarajevo"),
("Europe/Saratov", "Saratov"),
("Europe/Simferopol", "Simferopol"),
("Europe/Skopje", "Skopje"),
("Europe/Sofia", "Sofia"),
("Europe/Stockholm", "Stockholm"),
("Europe/Tallinn", "Tallinn"),
("Europe/Tirane", "Tirane"),
("Europe/Tiraspol", "Tiraspol"),
("Europe/Ulyanovsk", "Ulyanovsk"),
("Europe/Uzhgorod", "Uzhgorod"),
("Europe/Vaduz", "Vaduz"),
("Europe/Vatican", "Vatican"),
("Europe/Vienna", "Vienna"),
("Europe/Vilnius", "Vilnius"),
("Europe/Volgograd", "Volgograd"),
("Europe/Warsaw", "Warsaw"),
("Europe/Zagreb", "Zagreb"),
("Europe/Zaporozhye", "Zaporozhye"),
("Europe/Zurich", "Zurich"),
],
),
(
"Indian",
[
("Indian/Antananarivo", "Antananarivo"),
("Indian/Chagos", "Chagos"),
("Indian/Christmas", "Christmas"),
("Indian/Cocos", "Cocos"),
("Indian/Comoro", "Comoro"),
("Indian/Kerguelen", "Kerguelen"),
("Indian/Mahe", "Mahe"),
("Indian/Maldives", "Maldives"),
("Indian/Mauritius", "Mauritius"),
("Indian/Mayotte", "Mayotte"),
("Indian/Reunion", "Reunion"),
],
),
(
"Mexico",
[
("Mexico/BajaNorte", "BajaNorte"),
("Mexico/BajaSur", "BajaSur"),
("Mexico/General", "General"),
],
),
(
"Other",
[
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
),
(
"Pacific",
[
("Pacific/Apia", "Apia"),
("Pacific/Auckland", "Auckland"),
("Pacific/Bougainville", "Bougainville"),
("Pacific/Chatham", "Chatham"),
("Pacific/Chuuk", "Chuuk"),
("Pacific/Easter", "Easter"),
("Pacific/Efate", "Efate"),
("Pacific/Enderbury", "Enderbury"),
("Pacific/Fakaofo", "Fakaofo"),
("Pacific/Fiji", "Fiji"),
("Pacific/Funafuti", "Funafuti"),
("Pacific/Galapagos", "Galapagos"),
("Pacific/Gambier", "Gambier"),
("Pacific/Guadalcanal", "Guadalcanal"),
("Pacific/Guam", "Guam"),
("Pacific/Honolulu", "Honolulu"),
("Pacific/Johnston", "Johnston"),
("Pacific/Kanton", "Kanton"),
("Pacific/Kiritimati", "Kiritimati"),
("Pacific/Kosrae", "Kosrae"),
("Pacific/Kwajalein", "Kwajalein"),
("Pacific/Majuro", "Majuro"),
("Pacific/Marquesas", "Marquesas"),
("Pacific/Midway", "Midway"),
("Pacific/Nauru", "Nauru"),
("Pacific/Niue", "Niue"),
("Pacific/Norfolk", "Norfolk"),
("Pacific/Noumea", "Noumea"),
("Pacific/Pago_Pago", "Pago_Pago"),
("Pacific/Palau", "Palau"),
("Pacific/Pitcairn", "Pitcairn"),
("Pacific/Pohnpei", "Pohnpei"),
("Pacific/Ponape", "Ponape"),
("Pacific/Port_Moresby", "Port_Moresby"),
("Pacific/Rarotonga", "Rarotonga"),
("Pacific/Saipan", "Saipan"),
("Pacific/Samoa", "Samoa"),
("Pacific/Tahiti", "Tahiti"),
("Pacific/Tarawa", "Tarawa"),
("Pacific/Tongatapu", "Tongatapu"),
("Pacific/Truk", "Truk"),
("Pacific/Wake", "Wake"),
("Pacific/Wallis", "Wallis"),
("Pacific/Yap", "Yap"),
],
),
(
"US",
[
("US/Alaska", "Alaska"),
("US/Aleutian", "Aleutian"),
("US/Arizona", "Arizona"),
("US/Central", "Central"),
("US/East-Indiana", "East-Indiana"),
("US/Eastern", "Eastern"),
("US/Hawaii", "Hawaii"),
("US/Indiana-Starke", "Indiana-Starke"),
("US/Michigan", "Michigan"),
("US/Mountain", "Mountain"),
("US/Pacific", "Pacific"),
("US/Samoa", "Samoa"),
],
),
],
default="Asia/Ho_Chi_Minh",
max_length=50,
verbose_name="location",
),
),
]

View file

@ -0,0 +1,187 @@
# Generated by Django 3.2.16 on 2023-01-31 15:49
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0147_alter_profile_timezone"),
]
operations = [
migrations.CreateModel(
name="Course",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=128, verbose_name="course name")),
("about", models.TextField(verbose_name="organization description")),
("ending_time", models.DateTimeField(verbose_name="ending time")),
(
"is_public",
models.BooleanField(default=False, verbose_name="publicly visible"),
),
(
"slug",
models.SlugField(
help_text="Course name shown in URL",
max_length=128,
unique=True,
validators=[
django.core.validators.RegexValidator(
"^[-a-zA-Z0-9]+$", "Only alphanumeric and hyphens"
)
],
verbose_name="course slug",
),
),
(
"is_open",
models.BooleanField(
default=False, verbose_name="public registration"
),
),
(
"image_url",
models.CharField(
blank=True,
default="",
max_length=150,
verbose_name="course image",
),
),
(
"organizations",
models.ManyToManyField(
blank=True,
help_text="If private, only these organizations may see the course",
to="judge.Organization",
verbose_name="organizations",
),
),
],
),
migrations.CreateModel(
name="CourseRole",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"role",
models.CharField(
choices=[
("ST", "Student"),
("AS", "Assistant"),
("TE", "Teacher"),
],
default="ST",
max_length=2,
),
),
(
"course",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_of_course",
to="judge.profile",
verbose_name="user",
),
),
],
),
migrations.CreateModel(
name="CourseResource",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"files",
models.FileField(
blank=True, null=True, upload_to="", verbose_name="course files"
),
),
(
"description",
models.CharField(
blank=True, max_length=150, verbose_name="description"
),
),
("order", models.IntegerField(default=None, null=True)),
(
"is_public",
models.BooleanField(default=False, verbose_name="publicly visible"),
),
(
"course",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
],
),
migrations.CreateModel(
name="CourseAssignment",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("points", models.FloatField(verbose_name="points")),
(
"contest",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to="judge.contest",
verbose_name="contest",
),
),
(
"course",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
],
),
]

View file

@ -0,0 +1,682 @@
# Generated by Django 3.2.16 on 2023-02-02 02:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0148_course_courseassignment_courseresource_courserole"),
]
operations = [
migrations.AlterField(
model_name="notification",
name="category",
field=models.CharField(max_length=1000, verbose_name="category"),
),
migrations.AlterField(
model_name="profile",
name="timezone",
field=models.CharField(
choices=[
(
"Africa",
[
("Africa/Abidjan", "Abidjan"),
("Africa/Accra", "Accra"),
("Africa/Addis_Ababa", "Addis_Ababa"),
("Africa/Algiers", "Algiers"),
("Africa/Asmara", "Asmara"),
("Africa/Asmera", "Asmera"),
("Africa/Bamako", "Bamako"),
("Africa/Bangui", "Bangui"),
("Africa/Banjul", "Banjul"),
("Africa/Bissau", "Bissau"),
("Africa/Blantyre", "Blantyre"),
("Africa/Brazzaville", "Brazzaville"),
("Africa/Bujumbura", "Bujumbura"),
("Africa/Cairo", "Cairo"),
("Africa/Casablanca", "Casablanca"),
("Africa/Ceuta", "Ceuta"),
("Africa/Conakry", "Conakry"),
("Africa/Dakar", "Dakar"),
("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
("Africa/Djibouti", "Djibouti"),
("Africa/Douala", "Douala"),
("Africa/El_Aaiun", "El_Aaiun"),
("Africa/Freetown", "Freetown"),
("Africa/Gaborone", "Gaborone"),
("Africa/Harare", "Harare"),
("Africa/Johannesburg", "Johannesburg"),
("Africa/Juba", "Juba"),
("Africa/Kampala", "Kampala"),
("Africa/Khartoum", "Khartoum"),
("Africa/Kigali", "Kigali"),
("Africa/Kinshasa", "Kinshasa"),
("Africa/Lagos", "Lagos"),
("Africa/Libreville", "Libreville"),
("Africa/Lome", "Lome"),
("Africa/Luanda", "Luanda"),
("Africa/Lubumbashi", "Lubumbashi"),
("Africa/Lusaka", "Lusaka"),
("Africa/Malabo", "Malabo"),
("Africa/Maputo", "Maputo"),
("Africa/Maseru", "Maseru"),
("Africa/Mbabane", "Mbabane"),
("Africa/Mogadishu", "Mogadishu"),
("Africa/Monrovia", "Monrovia"),
("Africa/Nairobi", "Nairobi"),
("Africa/Ndjamena", "Ndjamena"),
("Africa/Niamey", "Niamey"),
("Africa/Nouakchott", "Nouakchott"),
("Africa/Ouagadougou", "Ouagadougou"),
("Africa/Porto-Novo", "Porto-Novo"),
("Africa/Sao_Tome", "Sao_Tome"),
("Africa/Timbuktu", "Timbuktu"),
("Africa/Tripoli", "Tripoli"),
("Africa/Tunis", "Tunis"),
("Africa/Windhoek", "Windhoek"),
],
),
(
"America",
[
("America/Adak", "Adak"),
("America/Anchorage", "Anchorage"),
("America/Anguilla", "Anguilla"),
("America/Antigua", "Antigua"),
("America/Araguaina", "Araguaina"),
(
"America/Argentina/Buenos_Aires",
"Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "Argentina/Cordoba"),
("America/Argentina/Jujuy", "Argentina/Jujuy"),
("America/Argentina/La_Rioja", "Argentina/La_Rioja"),
("America/Argentina/Mendoza", "Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "Argentina/Salta"),
("America/Argentina/San_Juan", "Argentina/San_Juan"),
("America/Argentina/San_Luis", "Argentina/San_Luis"),
("America/Argentina/Tucuman", "Argentina/Tucuman"),
("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
("America/Aruba", "Aruba"),
("America/Asuncion", "Asuncion"),
("America/Atikokan", "Atikokan"),
("America/Atka", "Atka"),
("America/Bahia", "Bahia"),
("America/Bahia_Banderas", "Bahia_Banderas"),
("America/Barbados", "Barbados"),
("America/Belem", "Belem"),
("America/Belize", "Belize"),
("America/Blanc-Sablon", "Blanc-Sablon"),
("America/Boa_Vista", "Boa_Vista"),
("America/Bogota", "Bogota"),
("America/Boise", "Boise"),
("America/Buenos_Aires", "Buenos_Aires"),
("America/Cambridge_Bay", "Cambridge_Bay"),
("America/Campo_Grande", "Campo_Grande"),
("America/Cancun", "Cancun"),
("America/Caracas", "Caracas"),
("America/Catamarca", "Catamarca"),
("America/Cayenne", "Cayenne"),
("America/Cayman", "Cayman"),
("America/Chicago", "Chicago"),
("America/Chihuahua", "Chihuahua"),
("America/Coral_Harbour", "Coral_Harbour"),
("America/Cordoba", "Cordoba"),
("America/Costa_Rica", "Costa_Rica"),
("America/Creston", "Creston"),
("America/Cuiaba", "Cuiaba"),
("America/Curacao", "Curacao"),
("America/Danmarkshavn", "Danmarkshavn"),
("America/Dawson", "Dawson"),
("America/Dawson_Creek", "Dawson_Creek"),
("America/Denver", "Denver"),
("America/Detroit", "Detroit"),
("America/Dominica", "Dominica"),
("America/Edmonton", "Edmonton"),
("America/Eirunepe", "Eirunepe"),
("America/El_Salvador", "El_Salvador"),
("America/Ensenada", "Ensenada"),
("America/Fort_Nelson", "Fort_Nelson"),
("America/Fort_Wayne", "Fort_Wayne"),
("America/Fortaleza", "Fortaleza"),
("America/Glace_Bay", "Glace_Bay"),
("America/Godthab", "Godthab"),
("America/Goose_Bay", "Goose_Bay"),
("America/Grand_Turk", "Grand_Turk"),
("America/Grenada", "Grenada"),
("America/Guadeloupe", "Guadeloupe"),
("America/Guatemala", "Guatemala"),
("America/Guayaquil", "Guayaquil"),
("America/Guyana", "Guyana"),
("America/Halifax", "Halifax"),
("America/Havana", "Havana"),
("America/Hermosillo", "Hermosillo"),
("America/Indiana/Indianapolis", "Indiana/Indianapolis"),
("America/Indiana/Knox", "Indiana/Knox"),
("America/Indiana/Marengo", "Indiana/Marengo"),
("America/Indiana/Petersburg", "Indiana/Petersburg"),
("America/Indiana/Tell_City", "Indiana/Tell_City"),
("America/Indiana/Vevay", "Indiana/Vevay"),
("America/Indiana/Vincennes", "Indiana/Vincennes"),
("America/Indiana/Winamac", "Indiana/Winamac"),
("America/Indianapolis", "Indianapolis"),
("America/Inuvik", "Inuvik"),
("America/Iqaluit", "Iqaluit"),
("America/Jamaica", "Jamaica"),
("America/Jujuy", "Jujuy"),
("America/Juneau", "Juneau"),
("America/Kentucky/Louisville", "Kentucky/Louisville"),
("America/Kentucky/Monticello", "Kentucky/Monticello"),
("America/Knox_IN", "Knox_IN"),
("America/Kralendijk", "Kralendijk"),
("America/La_Paz", "La_Paz"),
("America/Lima", "Lima"),
("America/Los_Angeles", "Los_Angeles"),
("America/Louisville", "Louisville"),
("America/Lower_Princes", "Lower_Princes"),
("America/Maceio", "Maceio"),
("America/Managua", "Managua"),
("America/Manaus", "Manaus"),
("America/Marigot", "Marigot"),
("America/Martinique", "Martinique"),
("America/Matamoros", "Matamoros"),
("America/Mazatlan", "Mazatlan"),
("America/Mendoza", "Mendoza"),
("America/Menominee", "Menominee"),
("America/Merida", "Merida"),
("America/Metlakatla", "Metlakatla"),
("America/Mexico_City", "Mexico_City"),
("America/Miquelon", "Miquelon"),
("America/Moncton", "Moncton"),
("America/Monterrey", "Monterrey"),
("America/Montevideo", "Montevideo"),
("America/Montreal", "Montreal"),
("America/Montserrat", "Montserrat"),
("America/Nassau", "Nassau"),
("America/New_York", "New_York"),
("America/Nipigon", "Nipigon"),
("America/Nome", "Nome"),
("America/Noronha", "Noronha"),
("America/North_Dakota/Beulah", "North_Dakota/Beulah"),
("America/North_Dakota/Center", "North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"North_Dakota/New_Salem",
),
("America/Nuuk", "Nuuk"),
("America/Ojinaga", "Ojinaga"),
("America/Panama", "Panama"),
("America/Pangnirtung", "Pangnirtung"),
("America/Paramaribo", "Paramaribo"),
("America/Phoenix", "Phoenix"),
("America/Port-au-Prince", "Port-au-Prince"),
("America/Port_of_Spain", "Port_of_Spain"),
("America/Porto_Acre", "Porto_Acre"),
("America/Porto_Velho", "Porto_Velho"),
("America/Puerto_Rico", "Puerto_Rico"),
("America/Punta_Arenas", "Punta_Arenas"),
("America/Rainy_River", "Rainy_River"),
("America/Rankin_Inlet", "Rankin_Inlet"),
("America/Recife", "Recife"),
("America/Regina", "Regina"),
("America/Resolute", "Resolute"),
("America/Rio_Branco", "Rio_Branco"),
("America/Rosario", "Rosario"),
("America/Santa_Isabel", "Santa_Isabel"),
("America/Santarem", "Santarem"),
("America/Santiago", "Santiago"),
("America/Santo_Domingo", "Santo_Domingo"),
("America/Sao_Paulo", "Sao_Paulo"),
("America/Scoresbysund", "Scoresbysund"),
("America/Shiprock", "Shiprock"),
("America/Sitka", "Sitka"),
("America/St_Barthelemy", "St_Barthelemy"),
("America/St_Johns", "St_Johns"),
("America/St_Kitts", "St_Kitts"),
("America/St_Lucia", "St_Lucia"),
("America/St_Thomas", "St_Thomas"),
("America/St_Vincent", "St_Vincent"),
("America/Swift_Current", "Swift_Current"),
("America/Tegucigalpa", "Tegucigalpa"),
("America/Thule", "Thule"),
("America/Thunder_Bay", "Thunder_Bay"),
("America/Tijuana", "Tijuana"),
("America/Toronto", "Toronto"),
("America/Tortola", "Tortola"),
("America/Vancouver", "Vancouver"),
("America/Virgin", "Virgin"),
("America/Whitehorse", "Whitehorse"),
("America/Winnipeg", "Winnipeg"),
("America/Yakutat", "Yakutat"),
("America/Yellowknife", "Yellowknife"),
],
),
(
"Antarctica",
[
("Antarctica/Casey", "Casey"),
("Antarctica/Davis", "Davis"),
("Antarctica/DumontDUrville", "DumontDUrville"),
("Antarctica/Macquarie", "Macquarie"),
("Antarctica/Mawson", "Mawson"),
("Antarctica/McMurdo", "McMurdo"),
("Antarctica/Palmer", "Palmer"),
("Antarctica/Rothera", "Rothera"),
("Antarctica/South_Pole", "South_Pole"),
("Antarctica/Syowa", "Syowa"),
("Antarctica/Troll", "Troll"),
("Antarctica/Vostok", "Vostok"),
],
),
("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
(
"Asia",
[
("Asia/Aden", "Aden"),
("Asia/Almaty", "Almaty"),
("Asia/Amman", "Amman"),
("Asia/Anadyr", "Anadyr"),
("Asia/Aqtau", "Aqtau"),
("Asia/Aqtobe", "Aqtobe"),
("Asia/Ashgabat", "Ashgabat"),
("Asia/Ashkhabad", "Ashkhabad"),
("Asia/Atyrau", "Atyrau"),
("Asia/Baghdad", "Baghdad"),
("Asia/Bahrain", "Bahrain"),
("Asia/Baku", "Baku"),
("Asia/Bangkok", "Bangkok"),
("Asia/Barnaul", "Barnaul"),
("Asia/Beirut", "Beirut"),
("Asia/Bishkek", "Bishkek"),
("Asia/Brunei", "Brunei"),
("Asia/Calcutta", "Calcutta"),
("Asia/Chita", "Chita"),
("Asia/Choibalsan", "Choibalsan"),
("Asia/Chongqing", "Chongqing"),
("Asia/Chungking", "Chungking"),
("Asia/Colombo", "Colombo"),
("Asia/Dacca", "Dacca"),
("Asia/Damascus", "Damascus"),
("Asia/Dhaka", "Dhaka"),
("Asia/Dili", "Dili"),
("Asia/Dubai", "Dubai"),
("Asia/Dushanbe", "Dushanbe"),
("Asia/Famagusta", "Famagusta"),
("Asia/Gaza", "Gaza"),
("Asia/Harbin", "Harbin"),
("Asia/Hebron", "Hebron"),
("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
("Asia/Hong_Kong", "Hong_Kong"),
("Asia/Hovd", "Hovd"),
("Asia/Irkutsk", "Irkutsk"),
("Asia/Istanbul", "Istanbul"),
("Asia/Jakarta", "Jakarta"),
("Asia/Jayapura", "Jayapura"),
("Asia/Jerusalem", "Jerusalem"),
("Asia/Kabul", "Kabul"),
("Asia/Kamchatka", "Kamchatka"),
("Asia/Karachi", "Karachi"),
("Asia/Kashgar", "Kashgar"),
("Asia/Kathmandu", "Kathmandu"),
("Asia/Katmandu", "Katmandu"),
("Asia/Khandyga", "Khandyga"),
("Asia/Kolkata", "Kolkata"),
("Asia/Krasnoyarsk", "Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
("Asia/Kuching", "Kuching"),
("Asia/Kuwait", "Kuwait"),
("Asia/Macao", "Macao"),
("Asia/Macau", "Macau"),
("Asia/Magadan", "Magadan"),
("Asia/Makassar", "Makassar"),
("Asia/Manila", "Manila"),
("Asia/Muscat", "Muscat"),
("Asia/Nicosia", "Nicosia"),
("Asia/Novokuznetsk", "Novokuznetsk"),
("Asia/Novosibirsk", "Novosibirsk"),
("Asia/Omsk", "Omsk"),
("Asia/Oral", "Oral"),
("Asia/Phnom_Penh", "Phnom_Penh"),
("Asia/Pontianak", "Pontianak"),
("Asia/Pyongyang", "Pyongyang"),
("Asia/Qatar", "Qatar"),
("Asia/Qostanay", "Qostanay"),
("Asia/Qyzylorda", "Qyzylorda"),
("Asia/Rangoon", "Rangoon"),
("Asia/Riyadh", "Riyadh"),
("Asia/Saigon", "Saigon"),
("Asia/Sakhalin", "Sakhalin"),
("Asia/Samarkand", "Samarkand"),
("Asia/Seoul", "Seoul"),
("Asia/Shanghai", "Shanghai"),
("Asia/Singapore", "Singapore"),
("Asia/Srednekolymsk", "Srednekolymsk"),
("Asia/Taipei", "Taipei"),
("Asia/Tashkent", "Tashkent"),
("Asia/Tbilisi", "Tbilisi"),
("Asia/Tehran", "Tehran"),
("Asia/Tel_Aviv", "Tel_Aviv"),
("Asia/Thimbu", "Thimbu"),
("Asia/Thimphu", "Thimphu"),
("Asia/Tokyo", "Tokyo"),
("Asia/Tomsk", "Tomsk"),
("Asia/Ujung_Pandang", "Ujung_Pandang"),
("Asia/Ulaanbaatar", "Ulaanbaatar"),
("Asia/Ulan_Bator", "Ulan_Bator"),
("Asia/Urumqi", "Urumqi"),
("Asia/Ust-Nera", "Ust-Nera"),
("Asia/Vientiane", "Vientiane"),
("Asia/Vladivostok", "Vladivostok"),
("Asia/Yakutsk", "Yakutsk"),
("Asia/Yangon", "Yangon"),
("Asia/Yekaterinburg", "Yekaterinburg"),
("Asia/Yerevan", "Yerevan"),
],
),
(
"Atlantic",
[
("Atlantic/Azores", "Azores"),
("Atlantic/Bermuda", "Bermuda"),
("Atlantic/Canary", "Canary"),
("Atlantic/Cape_Verde", "Cape_Verde"),
("Atlantic/Faeroe", "Faeroe"),
("Atlantic/Faroe", "Faroe"),
("Atlantic/Jan_Mayen", "Jan_Mayen"),
("Atlantic/Madeira", "Madeira"),
("Atlantic/Reykjavik", "Reykjavik"),
("Atlantic/South_Georgia", "South_Georgia"),
("Atlantic/St_Helena", "St_Helena"),
("Atlantic/Stanley", "Stanley"),
],
),
(
"Australia",
[
("Australia/ACT", "ACT"),
("Australia/Adelaide", "Adelaide"),
("Australia/Brisbane", "Brisbane"),
("Australia/Broken_Hill", "Broken_Hill"),
("Australia/Canberra", "Canberra"),
("Australia/Currie", "Currie"),
("Australia/Darwin", "Darwin"),
("Australia/Eucla", "Eucla"),
("Australia/Hobart", "Hobart"),
("Australia/LHI", "LHI"),
("Australia/Lindeman", "Lindeman"),
("Australia/Lord_Howe", "Lord_Howe"),
("Australia/Melbourne", "Melbourne"),
("Australia/NSW", "NSW"),
("Australia/North", "North"),
("Australia/Perth", "Perth"),
("Australia/Queensland", "Queensland"),
("Australia/South", "South"),
("Australia/Sydney", "Sydney"),
("Australia/Tasmania", "Tasmania"),
("Australia/Victoria", "Victoria"),
("Australia/West", "West"),
("Australia/Yancowinna", "Yancowinna"),
],
),
(
"Brazil",
[
("Brazil/Acre", "Acre"),
("Brazil/DeNoronha", "DeNoronha"),
("Brazil/East", "East"),
("Brazil/West", "West"),
],
),
(
"Canada",
[
("Canada/Atlantic", "Atlantic"),
("Canada/Central", "Central"),
("Canada/Eastern", "Eastern"),
("Canada/Mountain", "Mountain"),
("Canada/Newfoundland", "Newfoundland"),
("Canada/Pacific", "Pacific"),
("Canada/Saskatchewan", "Saskatchewan"),
("Canada/Yukon", "Yukon"),
],
),
(
"Chile",
[
("Chile/Continental", "Continental"),
("Chile/EasterIsland", "EasterIsland"),
],
),
(
"Etc",
[
("Etc/Greenwich", "Greenwich"),
("Etc/UCT", "UCT"),
("Etc/UTC", "UTC"),
("Etc/Universal", "Universal"),
("Etc/Zulu", "Zulu"),
],
),
(
"Europe",
[
("Europe/Amsterdam", "Amsterdam"),
("Europe/Andorra", "Andorra"),
("Europe/Astrakhan", "Astrakhan"),
("Europe/Athens", "Athens"),
("Europe/Belfast", "Belfast"),
("Europe/Belgrade", "Belgrade"),
("Europe/Berlin", "Berlin"),
("Europe/Bratislava", "Bratislava"),
("Europe/Brussels", "Brussels"),
("Europe/Bucharest", "Bucharest"),
("Europe/Budapest", "Budapest"),
("Europe/Busingen", "Busingen"),
("Europe/Chisinau", "Chisinau"),
("Europe/Copenhagen", "Copenhagen"),
("Europe/Dublin", "Dublin"),
("Europe/Gibraltar", "Gibraltar"),
("Europe/Guernsey", "Guernsey"),
("Europe/Helsinki", "Helsinki"),
("Europe/Isle_of_Man", "Isle_of_Man"),
("Europe/Istanbul", "Istanbul"),
("Europe/Jersey", "Jersey"),
("Europe/Kaliningrad", "Kaliningrad"),
("Europe/Kiev", "Kiev"),
("Europe/Kirov", "Kirov"),
("Europe/Kyiv", "Kyiv"),
("Europe/Lisbon", "Lisbon"),
("Europe/Ljubljana", "Ljubljana"),
("Europe/London", "London"),
("Europe/Luxembourg", "Luxembourg"),
("Europe/Madrid", "Madrid"),
("Europe/Malta", "Malta"),
("Europe/Mariehamn", "Mariehamn"),
("Europe/Minsk", "Minsk"),
("Europe/Monaco", "Monaco"),
("Europe/Moscow", "Moscow"),
("Europe/Nicosia", "Nicosia"),
("Europe/Oslo", "Oslo"),
("Europe/Paris", "Paris"),
("Europe/Podgorica", "Podgorica"),
("Europe/Prague", "Prague"),
("Europe/Riga", "Riga"),
("Europe/Rome", "Rome"),
("Europe/Samara", "Samara"),
("Europe/San_Marino", "San_Marino"),
("Europe/Sarajevo", "Sarajevo"),
("Europe/Saratov", "Saratov"),
("Europe/Simferopol", "Simferopol"),
("Europe/Skopje", "Skopje"),
("Europe/Sofia", "Sofia"),
("Europe/Stockholm", "Stockholm"),
("Europe/Tallinn", "Tallinn"),
("Europe/Tirane", "Tirane"),
("Europe/Tiraspol", "Tiraspol"),
("Europe/Ulyanovsk", "Ulyanovsk"),
("Europe/Uzhgorod", "Uzhgorod"),
("Europe/Vaduz", "Vaduz"),
("Europe/Vatican", "Vatican"),
("Europe/Vienna", "Vienna"),
("Europe/Vilnius", "Vilnius"),
("Europe/Volgograd", "Volgograd"),
("Europe/Warsaw", "Warsaw"),
("Europe/Zagreb", "Zagreb"),
("Europe/Zaporozhye", "Zaporozhye"),
("Europe/Zurich", "Zurich"),
],
),
(
"Indian",
[
("Indian/Antananarivo", "Antananarivo"),
("Indian/Chagos", "Chagos"),
("Indian/Christmas", "Christmas"),
("Indian/Cocos", "Cocos"),
("Indian/Comoro", "Comoro"),
("Indian/Kerguelen", "Kerguelen"),
("Indian/Mahe", "Mahe"),
("Indian/Maldives", "Maldives"),
("Indian/Mauritius", "Mauritius"),
("Indian/Mayotte", "Mayotte"),
("Indian/Reunion", "Reunion"),
],
),
(
"Mexico",
[
("Mexico/BajaNorte", "BajaNorte"),
("Mexico/BajaSur", "BajaSur"),
("Mexico/General", "General"),
],
),
(
"Other",
[
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
),
(
"Pacific",
[
("Pacific/Apia", "Apia"),
("Pacific/Auckland", "Auckland"),
("Pacific/Bougainville", "Bougainville"),
("Pacific/Chatham", "Chatham"),
("Pacific/Chuuk", "Chuuk"),
("Pacific/Easter", "Easter"),
("Pacific/Efate", "Efate"),
("Pacific/Enderbury", "Enderbury"),
("Pacific/Fakaofo", "Fakaofo"),
("Pacific/Fiji", "Fiji"),
("Pacific/Funafuti", "Funafuti"),
("Pacific/Galapagos", "Galapagos"),
("Pacific/Gambier", "Gambier"),
("Pacific/Guadalcanal", "Guadalcanal"),
("Pacific/Guam", "Guam"),
("Pacific/Honolulu", "Honolulu"),
("Pacific/Johnston", "Johnston"),
("Pacific/Kanton", "Kanton"),
("Pacific/Kiritimati", "Kiritimati"),
("Pacific/Kosrae", "Kosrae"),
("Pacific/Kwajalein", "Kwajalein"),
("Pacific/Majuro", "Majuro"),
("Pacific/Marquesas", "Marquesas"),
("Pacific/Midway", "Midway"),
("Pacific/Nauru", "Nauru"),
("Pacific/Niue", "Niue"),
("Pacific/Norfolk", "Norfolk"),
("Pacific/Noumea", "Noumea"),
("Pacific/Pago_Pago", "Pago_Pago"),
("Pacific/Palau", "Palau"),
("Pacific/Pitcairn", "Pitcairn"),
("Pacific/Pohnpei", "Pohnpei"),
("Pacific/Ponape", "Ponape"),
("Pacific/Port_Moresby", "Port_Moresby"),
("Pacific/Rarotonga", "Rarotonga"),
("Pacific/Saipan", "Saipan"),
("Pacific/Samoa", "Samoa"),
("Pacific/Tahiti", "Tahiti"),
("Pacific/Tarawa", "Tarawa"),
("Pacific/Tongatapu", "Tongatapu"),
("Pacific/Truk", "Truk"),
("Pacific/Wake", "Wake"),
("Pacific/Wallis", "Wallis"),
("Pacific/Yap", "Yap"),
],
),
(
"US",
[
("US/Alaska", "Alaska"),
("US/Aleutian", "Aleutian"),
("US/Arizona", "Arizona"),
("US/Central", "Central"),
("US/East-Indiana", "East-Indiana"),
("US/Eastern", "Eastern"),
("US/Hawaii", "Hawaii"),
("US/Indiana-Starke", "Indiana-Starke"),
("US/Michigan", "Michigan"),
("US/Mountain", "Mountain"),
("US/Pacific", "Pacific"),
("US/Samoa", "Samoa"),
],
),
],
default="Asia/Ho_Chi_Minh",
max_length=50,
verbose_name="location",
),
),
]

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.18 on 2023-02-20 21:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0150_alter_profile_timezone"),
]
operations = [
migrations.AddField(
model_name="comment",
name="content_type",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
preserve_default=False,
),
migrations.AddField(
model_name="comment",
name="object_id",
field=models.PositiveIntegerField(null=True),
preserve_default=False,
),
migrations.AlterField(
model_name="solution",
name="problem",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="solution",
to="judge.problem",
verbose_name="associated problem",
),
),
migrations.AddIndex(
model_name="comment",
index=models.Index(
fields=["content_type", "object_id"],
name="judge_comme_content_2dce05_idx",
),
),
]

View file

@ -0,0 +1,54 @@
from django.db import migrations, models
import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_comments(apps, schema_editor):
Comment = apps.get_model("judge", "Comment")
Problem = apps.get_model("judge", "Problem")
Solution = apps.get_model("judge", "Solution")
BlogPost = apps.get_model("judge", "BlogPost")
Contest = apps.get_model("judge", "Contest")
for comment in Comment.objects.all():
page = comment.page
try:
if page.startswith("p:"):
code = page[2:]
comment.linked_object = Problem.objects.get(code=code)
elif page.startswith("s:"):
code = page[2:]
comment.linked_object = Solution.objects.get(problem__code=code)
elif page.startswith("c:"):
key = page[2:]
comment.linked_object = Contest.objects.get(key=key)
elif page.startswith("b:"):
blog_id = page[2:]
comment.linked_object = BlogPost.objects.get(id=blog_id)
comment.save()
except ObjectDoesNotExist:
comment.delete()
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0151_comment_content_type"),
]
operations = [
migrations.RunPython(migrate_comments, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name="comment",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
migrations.AlterField(
model_name="comment",
name="object_id",
field=models.PositiveIntegerField(),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.18 on 2023-02-20 23:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0152_migrate_comments"),
]
operations = [
migrations.RemoveField(
model_name="comment",
name="page",
),
migrations.AlterField(
model_name="commentlock",
name="page",
field=models.CharField(
db_index=True, max_length=30, verbose_name="associated page"
),
),
]

File diff suppressed because one or more lines are too long

View file

@ -41,7 +41,7 @@ class CollabFilter:
def user_recommendations(self, user, problems, measure=DOT, limit=None, **kwargs): def user_recommendations(self, user, problems, measure=DOT, limit=None, **kwargs):
uid = user.id uid = user.id
problems_hash = hashlib.sha1(str(problems).encode()).hexdigest() problems_hash = hashlib.sha1(str(list(problems)).encode()).hexdigest()
cache_key = ":".join(map(str, [self.name, uid, measure, limit, problems_hash])) cache_key = ":".join(map(str, [self.name, uid, measure, limit, problems_hash]))
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
@ -65,16 +65,16 @@ class CollabFilter:
return res return res
# return a list of pid # return a list of pid
def problems_neighbors(self, problem, problemset, measure=DOT, limit=None): def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
pid = problem.id pid = problem.id
if pid >= len(self.problem_embeddings): if pid >= len(self.problem_embeddings):
return None return []
scores = self.compute_scores( scores = self.compute_scores(
self.problem_embeddings[pid], self.problem_embeddings, measure self.problem_embeddings[pid], self.problem_embeddings, measure
) )
res = [] res = []
for p in problemset: for p in problemset:
if p.id < len(scores): if p < len(scores):
res.append((scores[p.id], p)) res.append((scores[p], p))
res.sort(reverse=True, key=lambda x: x[0]) res.sort(reverse=True, key=lambda x: x[0])
return res[:limit] return res[:limit]

View file

@ -56,6 +56,7 @@ from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark from judge.models.bookmark import BookMark, MakeBookMark
from judge.models.course import Course
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"]) revisions.register(Problem, follow=["language_limits"])
@ -81,4 +82,5 @@ revisions.register(Rating)
revisions.register(PageVoteVoter) revisions.register(PageVoteVoter)
revisions.register(VolunteerProblemVote) revisions.register(VolunteerProblemVote)
revisions.register(MakeBookMark) revisions.register(MakeBookMark)
revisions.register(Course)
del revisions del revisions

View file

@ -3,10 +3,7 @@ from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from judge.models import Profile from judge.models.profile import Profile
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
__all__ = ["BookMark"] __all__ = ["BookMark"]
@ -26,6 +23,10 @@ class BookMark(models.Model):
return False return False
def page_object(self): def page_object(self):
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
try: try:
page = self.page page = self.page
if page.startswith("p:"): if page.startswith("p:"):

View file

@ -9,6 +9,8 @@ from django.db.models import CASCADE
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel from mptt.models import MPTTModel
from reversion.models import Version from reversion.models import Version
@ -22,10 +24,6 @@ from judge.utils.cachedict import CacheDict
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"] __all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
comment_validator = RegexValidator(
r"^[pcs]:[a-z0-9]+$|^b:\d+$", _(r"Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$")
)
class VersionRelation(GenericRelation): class VersionRelation(GenericRelation):
def __init__(self): def __init__(self):
@ -44,12 +42,9 @@ class VersionRelation(GenericRelation):
class Comment(MPTTModel): class Comment(MPTTModel):
author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
page = models.CharField( content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
max_length=30, object_id = models.PositiveIntegerField()
verbose_name=_("associated page"), linked_object = GenericForeignKey("content_type", "object_id")
db_index=True,
validators=[comment_validator],
)
score = models.IntegerField(verbose_name=_("votes"), default=0) score = models.IntegerField(verbose_name=_("votes"), default=0)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192) body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name=_("hide the comment"), default=0) hidden = models.BooleanField(verbose_name=_("hide the comment"), default=0)
@ -66,12 +61,15 @@ class Comment(MPTTModel):
class Meta: class Meta:
verbose_name = _("comment") verbose_name = _("comment")
verbose_name_plural = _("comments") verbose_name_plural = _("comments")
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
class MPTTMeta: class MPTTMeta:
order_insertion_by = ["-time"] order_insertion_by = ["-time"]
@classmethod @classmethod
def most_recent(cls, user, n, batch=None): def most_recent(cls, user, n, batch=None, organization=None):
queryset = ( queryset = (
cls.objects.filter(hidden=False) cls.objects.filter(hidden=False)
.select_related("author__user") .select_related("author__user")
@ -79,13 +77,12 @@ class Comment(MPTTModel):
.order_by("-id") .order_by("-id")
) )
problem_access = CacheDict( if organization:
lambda code: Problem.objects.get(code=code).is_accessible_by(user) queryset = queryset.filter(author__in=organization.members.all())
)
contest_access = CacheDict( problem_access = CacheDict(lambda p: p.is_accessible_by(user))
lambda key: Contest.objects.get(key=key).is_accessible_by(user) contest_access = CacheDict(lambda c: c.is_accessible_by(user))
) blog_access = CacheDict(lambda b: b.can_see(user))
blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user))
if n == -1: if n == -1:
n = len(queryset) n = len(queryset)
@ -99,112 +96,53 @@ class Comment(MPTTModel):
if not slice: if not slice:
break break
for comment in slice: for comment in slice:
if comment.page.startswith("p:") or comment.page.startswith("s:"): if isinstance(comment.linked_object, Problem):
try: if problem_access[comment.linked_object]:
if problem_access[comment.page[2:]]:
output.append(comment) output.append(comment)
except Problem.DoesNotExist: elif isinstance(comment.linked_object, Contest):
pass if contest_access[comment.linked_object]:
elif comment.page.startswith("c:"):
try:
if contest_access[comment.page[2:]]:
output.append(comment) output.append(comment)
except Contest.DoesNotExist: elif isinstance(comment.linked_object, BlogPost):
pass if blog_access[comment.linked_object]:
elif comment.page.startswith("b:"):
try:
if blog_access[comment.page[2:]]:
output.append(comment) output.append(comment)
except BlogPost.DoesNotExist: elif isinstance(comment.linked_object, Solution):
pass if problem_access[comment.linked_object.problem]:
else:
output.append(comment) output.append(comment)
if len(output) >= n: if len(output) >= n:
return output return output
return output return output
@cached_property @cached_property
def link(self): def page_title(self):
try: if isinstance(self.linked_object, Problem):
link = None return self.linked_object.name
if self.page.startswith("p:"): elif isinstance(self.linked_object, Contest):
link = reverse("problem_detail", args=(self.page[2:],)) return self.linked_object.name
elif self.page.startswith("c:"): elif isinstance(self.linked_object, Solution):
link = reverse("contest_view", args=(self.page[2:],)) return _("Editorial for ") + self.linked_object.problem.name
elif self.page.startswith("b:"): elif isinstance(self.linked_object, BlogPost):
key = "blog_slug:%s" % self.page[2:] return self.linked_object.title
slug = cache.get(key)
if slug is None:
try:
slug = BlogPost.objects.get(id=self.page[2:]).slug
except ObjectDoesNotExist:
slug = ""
cache.set(key, slug, 3600)
link = reverse("blog_post", args=(self.page[2:], slug))
elif self.page.startswith("s:"):
link = reverse("problem_editorial", args=(self.page[2:],))
except Exception:
link = "invalid"
return link
@classmethod
def get_page_title(cls, page):
try:
if page.startswith("p:"):
return Problem.objects.values_list("name", flat=True).get(code=page[2:])
elif page.startswith("c:"):
return Contest.objects.values_list("name", flat=True).get(key=page[2:])
elif page.startswith("b:"):
return BlogPost.objects.values_list("title", flat=True).get(id=page[2:])
elif page.startswith("s:"):
return _("Editorial for %s") % Problem.objects.values_list(
"name", flat=True
).get(code=page[2:])
return "<unknown>"
except ObjectDoesNotExist:
return "<deleted>"
@cached_property @cached_property
def page_title(self): def link(self):
return self.get_page_title(self.page) if isinstance(self.linked_object, Problem):
return reverse("problem_detail", args=(self.linked_object.code,))
elif isinstance(self.linked_object, Contest):
return reverse("contest_view", args=(self.linked_object.key,))
elif isinstance(self.linked_object, Solution):
return reverse("problem_editorial", args=(self.linked_object.problem.code,))
elif isinstance(self.linked_object, BlogPost):
return reverse(
"blog_post",
args=(
self.object_id,
self.linked_object.slug,
),
)
def get_absolute_url(self): def get_absolute_url(self):
return "%s#comment-%d" % (self.link, self.id) return "%s#comment-%d" % (self.link, self.id)
@cached_property
def page_object(self):
try:
page = self.page
if page.startswith("p:"):
return Problem.objects.get(code=page[2:])
elif page.startswith("c:"):
return Contest.objects.get(key=page[2:])
elif page.startswith("b:"):
return BlogPost.objects.get(id=page[2:])
elif page.startswith("s:"):
return Solution.objects.get(problem__code=page[2:])
return None
except ObjectDoesNotExist:
return None
def __str__(self):
return "%(page)s by %(user)s" % {
"page": self.page,
"user": self.author.user.username,
}
# Only use this when queried with
# .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id)))
# It's rather stupid to put a query specific property on the model, but the alternative requires
# digging Django internals, and could not be guaranteed to work forever.
# Hence it is left here for when the alternative breaks.
# @property
# def vote_score(self):
# queryset = self.votes.all()
# if not queryset:
# return 0
# return queryset[0].score
class CommentVote(models.Model): class CommentVote(models.Model):
voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE) voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE)
@ -222,7 +160,6 @@ class CommentLock(models.Model):
max_length=30, max_length=30,
verbose_name=_("associated page"), verbose_name=_("associated page"),
db_index=True, db_index=True,
validators=[comment_validator],
) )
class Meta: class Meta:
@ -244,7 +181,7 @@ class Notification(models.Model):
Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE
) )
read = models.BooleanField(verbose_name=_("read"), default=False) read = models.BooleanField(verbose_name=_("read"), default=False)
category = models.CharField(verbose_name=_("category"), max_length=50) category = models.CharField(verbose_name=_("category"), max_length=1000)
html_link = models.TextField( html_link = models.TextField(
default="", default="",
verbose_name=_("html link to comments, used for non-comments"), verbose_name=_("html link to comments, used for non-comments"),

View file

@ -6,6 +6,7 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.contenttypes.fields import GenericRelation
from jsonfield import JSONField from jsonfield import JSONField
from lupa import LuaRuntime from lupa import LuaRuntime
from moss import ( from moss import (
@ -297,6 +298,7 @@ class Contest(models.Model):
validators=[MinValueValidator(0), MaxValueValidator(10)], validators=[MinValueValidator(0), MaxValueValidator(10)],
help_text=_("Number of digits to round points to."), help_text=_("Number of digits to round points to."),
) )
comments = GenericRelation("Comment")
@cached_property @cached_property
def format_class(self): def format_class(self):

184
judge/models/course.py Normal file
View file

@ -0,0 +1,184 @@
from django.core.validators import RegexValidator
from django.db import models
from django.utils.translation import gettext, gettext_lazy as _
from judge.models import Contest
from judge.models.profile import Organization, Profile
__all__ = [
"Course",
"CourseRole",
"CourseResource",
"CourseAssignment",
]
course_directory_file = ""
class Course(models.Model):
name = models.CharField(
max_length=128,
verbose_name=_("course name"),
)
about = models.TextField(verbose_name=_("organization description"))
ending_time = models.DateTimeField(
verbose_name=_("ending time"),
)
is_public = models.BooleanField(
verbose_name=_("publicly visible"),
default=False,
)
organizations = models.ManyToManyField(
Organization,
blank=True,
verbose_name=_("organizations"),
help_text=_("If private, only these organizations may see the course"),
)
slug = models.SlugField(
max_length=128,
verbose_name=_("course slug"),
help_text=_("Course name shown in URL"),
unique=True,
validators=[
RegexValidator("^[-a-zA-Z0-9]+$", _("Only alphanumeric and hyphens"))
],
)
is_open = models.BooleanField(
verbose_name=_("public registration"),
default=False,
)
image_url = models.CharField(
verbose_name=_("course image"),
default="",
max_length=150,
blank=True,
)
def __str__(self):
return self.name
@classmethod
def is_editable_by(cls, course, profile):
userquery = CourseRole.objects.filter(course=course, user=profile)
if userquery.exists():
if userquery[0].role == "AS" or userquery[0].role == "TE":
return True
return False
@classmethod
def is_accessible_by(cls, course, profile):
userqueryset = CourseRole.objects.filter(course=course, user=profile)
if userqueryset.exists():
return True
else:
return False
@classmethod
def get_students(cls, course):
return CourseRole.objects.filter(course=course, role="ST").values("user")
@classmethod
def get_assistants(cls, course):
return CourseRole.objects.filter(course=course, role="AS").values("user")
@classmethod
def get_teachers(cls, course):
return CourseRole.objects.filter(course=course, role="TE").values("user")
@classmethod
def add_student(cls, course, profiles):
for profile in profiles:
CourseRole.make_role(course=course, user=profile, role="ST")
@classmethod
def add_teachers(cls, course, profiles):
for profile in profiles:
CourseRole.make_role(course=course, user=profile, role="TE")
@classmethod
def add_assistants(cls, course, profiles):
for profile in profiles:
CourseRole.make_role(course=course, user=profile, role="AS")
class CourseRole(models.Model):
course = models.ForeignKey(
Course,
verbose_name=_("course"),
on_delete=models.CASCADE,
db_index=True,
)
user = models.ForeignKey(
Profile,
verbose_name=_("user"),
on_delete=models.CASCADE,
related_name=_("user_of_course"),
)
class RoleInCourse(models.TextChoices):
STUDENT = "ST", _("Student")
ASSISTANT = "AS", _("Assistant")
TEACHER = "TE", _("Teacher")
role = models.CharField(
max_length=2,
choices=RoleInCourse.choices,
default=RoleInCourse.STUDENT,
)
@classmethod
def make_role(self, course, user, role):
userqueryset = CourseRole.objects.filter(course=course, user=user)
if userqueryset.exists():
userqueryset[0].role = role
else:
couresrole = CourseRole()
couresrole.course = course
couresrole.user = user
couresrole.role = role
couresrole.save()
class CourseResource(models.Model):
course = models.ForeignKey(
Course,
verbose_name=_("course"),
on_delete=models.CASCADE,
db_index=True,
)
files = models.FileField(
verbose_name=_("course files"),
null=True,
blank=True,
upload_to=course_directory_file,
)
description = models.CharField(
verbose_name=_("description"),
blank=True,
max_length=150,
)
order = models.IntegerField(null=True, default=None)
is_public = models.BooleanField(
verbose_name=_("publicly visible"),
default=False,
)
def __str__(self):
return f"Resource - {self.pk}"
class CourseAssignment(models.Model):
course = models.ForeignKey(
Course,
verbose_name=_("course"),
on_delete=models.CASCADE,
db_index=True,
)
contest = models.ForeignKey(
Contest,
verbose_name=_("contest"),
on_delete=models.CASCADE,
)
points = models.FloatField(
verbose_name=_("points"),
)

View file

@ -5,10 +5,14 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel from mptt.models import MPTTModel
from judge.models.profile import Organization, Profile from judge.models.profile import Organization, Profile
from judge.models.pagevote import PageVote
from judge.models.bookmark import BookMark
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"] __all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
@ -91,6 +95,7 @@ class BlogPost(models.Model):
is_organization_private = models.BooleanField( is_organization_private = models.BooleanField(
verbose_name=_("private to organizations"), default=False verbose_name=_("private to organizations"), default=False
) )
comments = GenericRelation("Comment")
def __str__(self): def __str__(self):
return self.title return self.title
@ -125,6 +130,18 @@ class BlogPost(models.Model):
and self.authors.filter(id=user.profile.id).exists() and self.authors.filter(id=user.profile.id).exists()
) )
@cached_property
def pagevote(self):
page = "b:%s" % self.id
pagevote, _ = PageVote.objects.get_or_create(page=page)
return pagevote
@cached_property
def bookmark(self):
page = "b:%s" % self.id
bookmark, _ = BookMark.objects.get_or_create(page=page)
return bookmark
class Meta: class Meta:
permissions = (("edit_all_post", _("Edit all posts")),) permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post") verbose_name = _("blog post")

View file

@ -2,7 +2,7 @@ from django.db import models
from django.db.models import CASCADE from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from judge.models import Profile from judge.models.profile import Profile
__all__ = ["PageVote", "PageVoteVoter"] __all__ = ["PageVote", "PageVoteVoter"]

View file

@ -6,13 +6,15 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.cache import cache from django.core.cache import cache
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models from django.db import models
from django.db.models import CASCADE, F, FilteredRelation, Q, SET_NULL from django.db.models import CASCADE, F, FilteredRelation, Q, SET_NULL, Exists, OuterRef
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from judge.fulltext import SearchQuerySet from judge.fulltext import SearchQuerySet
from judge.models.pagevote import PageVote
from judge.models.bookmark import BookMark
from judge.models.profile import Organization, Profile from judge.models.profile import Organization, Profile
from judge.models.runtime import Language from judge.models.runtime import Language
from judge.user_translations import gettext as user_gettext from judge.user_translations import gettext as user_gettext
@ -268,6 +270,7 @@ class Problem(models.Model):
objects = TranslatedProblemQuerySet.as_manager() objects = TranslatedProblemQuerySet.as_manager()
tickets = GenericRelation("Ticket") tickets = GenericRelation("Ticket")
comments = GenericRelation("Comment")
organizations = models.ManyToManyField( organizations = models.ManyToManyField(
Organization, Organization,
@ -369,9 +372,9 @@ class Problem(models.Model):
) )
@classmethod @classmethod
def get_visible_problems(cls, user): def get_visible_problems(cls, user, profile=None):
# Do unauthenticated check here so we can skip authentication checks later on. # Do unauthenticated check here so we can skip authentication checks later on.
if not user.is_authenticated: if not user.is_authenticated or not user:
return cls.get_public_problems() return cls.get_public_problems()
# Conditions for visible problem: # Conditions for visible problem:
@ -383,7 +386,7 @@ class Problem(models.Model):
# - not is_organization_private or in organization or `judge.see_organization_problem` # - not is_organization_private or in organization or `judge.see_organization_problem`
# - author or curator or tester # - author or curator or tester
queryset = cls.objects.defer("description") queryset = cls.objects.defer("description")
profile = profile or user.profile
if not ( if not (
user.has_perm("judge.see_private_problem") user.has_perm("judge.see_private_problem")
or user.has_perm("judge.edit_all_problem") or user.has_perm("judge.edit_all_problem")
@ -393,13 +396,25 @@ class Problem(models.Model):
# Either not organization private or in the organization. # Either not organization private or in the organization.
q &= Q(is_organization_private=False) | Q( q &= Q(is_organization_private=False) | Q(
is_organization_private=True, is_organization_private=True,
organizations__in=user.profile.organizations.all(), organizations__in=profile.organizations.all(),
) )
# Authors, curators, and testers should always have access, so OR at the very end. # Authors, curators, and testers should always have access, so OR at the very end.
q |= Q(authors=user.profile) filter = Exists(
q |= Q(curators=user.profile) Problem.authors.through.objects.filter(
q |= Q(testers=user.profile) problem=OuterRef("pk"), profile=profile
)
)
filter |= Exists(
Problem.curators.through.objects.filter(
problem=OuterRef("pk"), profile=profile
)
)
filter |= Exists(
Problem.testers.through.objects.filter(
problem=OuterRef("pk"), profile=profile
)
)
queryset = queryset.filter(q) queryset = queryset.filter(q)
return queryset return queryset
@ -432,6 +447,18 @@ class Problem(models.Model):
def usable_common_names(self): def usable_common_names(self):
return set(self.usable_languages.values_list("common_name", flat=True)) return set(self.usable_languages.values_list("common_name", flat=True))
@cached_property
def pagevote(self):
page = "p:%s" % self.code
pagevote, _ = PageVote.objects.get_or_create(page=page)
return pagevote
@cached_property
def bookmark(self):
page = "p:%s" % self.code
bookmark, _ = BookMark.objects.get_or_create(page=page)
return bookmark
@property @property
def usable_languages(self): def usable_languages(self):
return self.allowed_languages.filter( return self.allowed_languages.filter(
@ -532,6 +559,10 @@ class Problem(models.Model):
return result return result
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.pdf_description:
self.pdf_description.name = problem_directory_file_helper(
self.code, self.pdf_description.name
)
super(Problem, self).save(*args, **kwargs) super(Problem, self).save(*args, **kwargs)
if self.code != self.__original_code: if self.code != self.__original_code:
try: try:
@ -632,7 +663,7 @@ class LanguageTemplate(models.Model):
class Solution(models.Model): class Solution(models.Model):
problem = models.OneToOneField( problem = models.OneToOneField(
Problem, Problem,
on_delete=SET_NULL, on_delete=CASCADE,
verbose_name=_("associated problem"), verbose_name=_("associated problem"),
null=True, null=True,
blank=True, blank=True,
@ -642,6 +673,7 @@ class Solution(models.Model):
publish_on = models.DateTimeField(verbose_name=_("publish date")) publish_on = models.DateTimeField(verbose_name=_("publish date"))
authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True) authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
content = models.TextField(verbose_name=_("editorial content")) content = models.TextField(verbose_name=_("editorial content"))
comments = GenericRelation("Comment")
def get_absolute_url(self): def get_absolute_url(self):
problem = self.problem problem = self.problem

View file

@ -33,6 +33,10 @@ class Organization(models.Model):
max_length=128, max_length=128,
verbose_name=_("organization slug"), verbose_name=_("organization slug"),
help_text=_("Organization name shown in URL"), help_text=_("Organization name shown in URL"),
unique=True,
validators=[
RegexValidator("^[-a-zA-Z0-9]+$", _("Only alphanumeric and hyphens"))
],
) )
short_name = models.CharField( short_name = models.CharField(
max_length=20, max_length=20,
@ -329,16 +333,24 @@ class Profile(models.Model):
def css_class(self): def css_class(self):
return self.get_user_css_class(self.display_rank, self.rating) return self.get_user_css_class(self.display_rank, self.rating)
def get_friends(self): # list of usernames, including you def get_friends(self): # list of ids, including you
friend_obj = self.following_users.all() friend_obj = self.following_users.prefetch_related("users")
ret = set() ret = []
if friend_obj: if friend_obj:
ret = set(friend.username for friend in friend_obj[0].users.all()) ret = [friend.id for friend in friend_obj[0].users.all()]
ret.append(self.id)
ret.add(self.username)
return ret return ret
def can_edit_organization(self, org):
if not self.user.is_authenticated:
return False
profile_id = self.id
return (
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id
or self.user.is_superuser
)
class Meta: class Meta:
permissions = ( permissions = (
("test_site", "Shows in-progress development stuff"), ("test_site", "Shows in-progress development stuff"),
@ -381,7 +393,9 @@ class OrganizationRequest(models.Model):
class Friend(models.Model): class Friend(models.Model):
users = models.ManyToManyField(Profile) users = models.ManyToManyField(Profile)
current_user = models.ForeignKey( current_user = models.ForeignKey(
Profile, related_name="following_users", on_delete=CASCADE Profile,
related_name="following_users",
on_delete=CASCADE,
) )
@classmethod @classmethod

View file

@ -22,37 +22,48 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute( cursor.execute(
""" """
SELECT max_points_table.problem_code, SELECT submission.problem_code,
max_points_table.problem_name, submission.problem_name,
max_points_table.max_points, submission.max_points,
judge_submission.id, submission.sub_id,
judge_submission.date, submission.sub_date,
judge_submission.case_points, submission.case_points,
judge_submission.case_total, submission.case_total,
judge_submission.result, submission.result,
judge_language.short_name, judge_language.short_name,
judge_language.key judge_language.key
FROM
(SELECT max_points_table.problem_code problem_code,
max_points_table.problem_name problem_name,
max_points_table.max_points max_points,
judge_submission.id sub_id,
judge_submission.date sub_date,
judge_submission.case_points case_points,
judge_submission.case_total case_total,
judge_submission.result result,
judge_submission.language_id language_id
FROM judge_submission FROM judge_submission
JOIN (SELECT judge_problem.id problem_id, JOIN (
SELECT judge_problem.id problem_id,
judge_problem.name problem_name, judge_problem.name problem_name,
judge_problem.code problem_code, judge_problem.code problem_code,
MAX(judge_submission.points) AS max_points MAX(judge_submission.points) AS max_points
FROM judge_problem FROM judge_problem
INNER JOIN judge_submission ON (judge_problem.id = judge_submission.problem_id) INNER JOIN judge_submission
WHERE (judge_problem.is_public = True AND ON (judge_problem.id = judge_submission.problem_id)
judge_problem.is_organization_private = False AND WHERE (judge_problem.is_public = True AND judge_problem.is_organization_private = False AND judge_submission.points IS NOT NULL AND judge_submission.user_id = %s)
judge_submission.points IS NOT NULL AND
judge_submission.user_id = %s)
GROUP BY judge_problem.id GROUP BY judge_problem.id
HAVING MAX(judge_submission.points) > 0.0) AS max_points_table HAVING MAX(judge_submission.points) > 0.0
ON (judge_submission.problem_id = max_points_table.problem_id AND ) AS max_points_table
judge_submission.points = max_points_table.max_points AND ON (judge_submission.problem_id = max_points_table.problem_id AND judge_submission.points = max_points_table.max_points AND judge_submission.user_id = %s)
judge_submission.user_id = %s)
JOIN judge_language
ON judge_submission.language_id = judge_language.id
GROUP BY max_points_table.problem_id GROUP BY max_points_table.problem_id
ORDER BY max_points DESC, judge_submission.date DESC ORDER BY max_points DESC, judge_submission.date DESC
LIMIT %s OFFSET %s LIMIT %s
OFFSET %s
) AS submission
JOIN judge_language
ON submission.language_id = judge_language.id
ORDER BY submission.max_points DESC, submission.sub_date DESC;
""", """,
(user.id, user.id, end - start + 1, start), (user.id, user.id, end - start + 1, start),
) )

View file

@ -20,6 +20,23 @@ def rescore_contest(self, contest_key):
self, participations.count(), stage=_("Recalculating contest scores") self, participations.count(), stage=_("Recalculating contest scores")
) as p: ) as p:
for participation in participations.iterator(): for participation in participations.iterator():
for contest_submission in participation.submissions.iterator():
submission = contest_submission.submission
contest_problem = contest_submission.problem
contest_submission.points = round(
submission.case_points
/ submission.case_total
* contest_problem.points
if submission.case_total > 0
else 0,
3,
)
if (
not contest_problem.partial
and contest_submission.points != contest_problem.points
):
contest_submission.points = 0
contest_submission.save()
participation.recompute_results() participation.recompute_results()
rescored += 1 rescored += 1
if rescored % 10 == 0: if rescored % 10 == 0:

View file

@ -191,6 +191,9 @@ class DiggPaginator(ExPaginator):
# validate padding value # validate padding value
max_padding = int(math.ceil(self.body / 2.0) - 1) max_padding = int(math.ceil(self.body / 2.0) - 1)
self.padding = kwargs.pop("padding", min(4, max_padding)) self.padding = kwargs.pop("padding", min(4, max_padding))
count_override = kwargs.pop("count", None)
if count_override is not None:
self.__dict__["count"] = count_override
if self.padding > max_padding: if self.padding > max_padding:
raise ValueError("padding too large for body (max %d)" % max_padding) raise ValueError("padding too large for body (max %d)" % max_padding)
super(DiggPaginator, self).__init__(*args, **kwargs) super(DiggPaginator, self).__init__(*args, **kwargs)

View file

@ -0,0 +1,152 @@
import collections
import inspect
from math import ceil
from django.core.paginator import EmptyPage, InvalidPage
from django.http import Http404
from django.utils.functional import cached_property
from django.utils.inspect import method_has_no_args
class InfinitePage(collections.abc.Sequence):
def __init__(
self, object_list, number, unfiltered_queryset, page_size, pad_pages, paginator
):
self.object_list = list(object_list)
self.number = number
self.unfiltered_queryset = unfiltered_queryset
self.page_size = page_size
self.pad_pages = pad_pages
self.num_pages = 1e3000
self.paginator = paginator
def __repr__(self):
return "<Page %s of many>" % self.number
def __len__(self):
return len(self.object_list)
def __getitem__(self, index):
return self.object_list[index]
@cached_property
def _after_up_to_pad(self):
first_after = self.number * self.page_size
padding_length = self.pad_pages * self.page_size
queryset = self.unfiltered_queryset[
first_after : first_after + padding_length + 1
]
c = getattr(queryset, "count", None)
if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
return c()
return len(queryset)
def has_next(self):
return self._after_up_to_pad > 0
def has_previous(self):
return self.number > 1
def has_other_pages(self):
return self.has_previous() or self.has_next()
def next_page_number(self):
if not self.has_next():
raise EmptyPage()
return self.number + 1
def previous_page_number(self):
if self.number <= 1:
raise EmptyPage()
return self.number - 1
def start_index(self):
return (self.page_size * (self.number - 1)) + 1
def end_index(self):
return self.start_index() + len(self.object_list)
@cached_property
def main_range(self):
start = max(1, self.number - self.pad_pages)
end = self.number + min(
int(ceil(self._after_up_to_pad / self.page_size)), self.pad_pages
)
return range(start, end + 1)
@cached_property
def leading_range(self):
return range(1, min(3, self.main_range[0]))
@cached_property
def has_trailing(self):
return self._after_up_to_pad > self.pad_pages * self.page_size
@cached_property
def page_range(self):
result = list(self.leading_range)
main_range = self.main_range
# Add ... element if there is space in between.
if result and result[-1] + 1 < self.main_range[0]:
result.append(False)
result += list(main_range)
# Add ... element if there are elements after main_range.
if self.has_trailing:
result.append(False)
return result
class DummyPaginator:
is_infinite = True
def __init__(self, per_page):
self.per_page = per_page
def infinite_paginate(queryset, page, page_size, pad_pages, paginator=None):
if page < 1:
raise EmptyPage()
sliced = queryset[(page - 1) * page_size : page * page_size]
if page > 1 and not sliced:
raise EmptyPage()
return InfinitePage(sliced, page, queryset, page_size, pad_pages, paginator)
class InfinitePaginationMixin:
pad_pages = 4
@property
def use_infinite_pagination(self):
return True
def paginate_queryset(self, queryset, page_size):
if not self.use_infinite_pagination:
paginator, page, object_list, has_other = super().paginate_queryset(
queryset, page_size
)
paginator.is_infinite = False
return paginator, page, object_list, has_other
page_kwarg = self.page_kwarg
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
try:
page_number = int(page)
except ValueError:
raise Http404("Page cannot be converted to an int.")
try:
paginator = DummyPaginator(page_size)
page = infinite_paginate(
queryset, page_number, page_size, self.pad_pages, paginator
)
return paginator, page, page.object_list, page.has_other_pages()
except InvalidPage as e:
raise Http404(
"Invalid page (%(page_number)s): %(message)s"
% {
"page_number": page_number,
"message": str(e),
}
)

View file

@ -1,6 +1,8 @@
from collections import defaultdict from collections import defaultdict
from math import e from math import e
import os, zipfile import os, zipfile
from datetime import datetime
import random
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -10,19 +12,24 @@ from django.utils import timezone
from django.utils.translation import gettext as _, gettext_noop from django.utils.translation import gettext as _, gettext_noop
from judge.models import Problem, Submission from judge.models import Problem, Submission
from judge.ml.collab_filter import CollabFilter
__all__ = [ __all__ = [
"contest_completed_ids", "contest_completed_ids",
"get_result_data", "get_result_data",
"user_completed_ids", "user_completed_ids",
"user_authored_ids",
"user_editable_ids", "user_editable_ids",
"user_tester_ids",
] ]
def user_authored_ids(profile): def user_tester_ids(profile):
result = set(Problem.objects.filter(authors=profile).values_list("id", flat=True)) return set(
return result Problem.testers.through.objects.filter(profile=profile).values_list(
"problem_id", flat=True
)
)
def user_editable_ids(profile): def user_editable_ids(profile):
@ -229,3 +236,26 @@ def hot_problems(duration, limit):
cache.set(cache_key, qs, 900) cache.set(cache_key, qs, 900)
return qs return qs
def get_related_problems(profile, problem, limit=8):
if not profile or not settings.ML_OUTPUT_PATH:
return None
cache_key = "related_problems:%d:%d" % (profile.id, problem.id)
qs = cache.get(cache_key)
if qs is not None:
return qs
problemset = Problem.get_visible_problems(profile.user).values_list("id", flat=True)
problemset = problemset.exclude(id__in=user_completed_ids(profile))
problemset = problemset.exclude(id=problem.id)
cf_model = CollabFilter("collab_filter")
results = cf_model.problem_neighbors(
problem, problemset, CollabFilter.DOT, limit
) + cf_model.problem_neighbors(problem, problemset, CollabFilter.COSINE, limit)
results = list(set([i[1] for i in results]))
seed = datetime.now().strftime("%d%m%Y")
random.Random(seed).shuffle(results)
results = results[:limit]
results = [Problem.objects.get(id=i) for i in results]
cache.set(cache_key, results, 21600)
return results

View file

@ -7,8 +7,8 @@ from django.utils.translation import ugettext as _
from django.views.generic import ListView from django.views.generic import ListView
from judge.comments import CommentedDetailView from judge.comments import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView, PageVoteListView from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView from judge.views.bookmark import BookMarkDetailView
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
Comment, Comment,
@ -26,28 +26,16 @@ from judge.utils.diggpaginator import DiggPaginator
from judge.utils.problems import user_completed_ids from judge.utils.problems import user_completed_ids
from judge.utils.tickets import filter_visible_tickets from judge.utils.tickets import filter_visible_tickets
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.views.feed import FeedView
# General view for all content list on home feed # General view for all content list on home feed
class FeedView(ListView): class HomeFeedView(FeedView):
template_name = "blog/list.html" template_name = "blog/list.html"
title = None title = None
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
return DiggPaginator(
queryset,
per_page,
body=6,
padding=2,
orphans=orphans,
allow_empty_first_page=allow_empty_first_page,
**kwargs
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(FeedView, self).get_context_data(**kwargs) context = super(HomeFeedView, self).get_context_data(**kwargs)
context["has_clarifications"] = False context["has_clarifications"] = False
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
participation = self.request.profile.current_contest participation = self.request.profile.current_contest
@ -60,22 +48,16 @@ class FeedView(ListView):
if participation.contest.is_editable_by(self.request.user): if participation.contest.is_editable_by(self.request.user):
context["can_edit_contest"] = True context["can_edit_contest"] = True
context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page))
context["user_count"] = lazy(Profile.objects.count, int, int)
context["problem_count"] = lazy(
Problem.objects.filter(is_public=True).count, int, int
)
context["submission_count"] = lazy(Submission.objects.count, int, int)
context["language_count"] = lazy(Language.objects.count, int, int)
now = timezone.now() now = timezone.now()
visible_contests = ( visible_contests = (
Contest.get_visible_contests(self.request.user, show_own_contests_only=True) Contest.get_visible_contests(self.request.user, show_own_contests_only=True)
.filter(is_visible=True) .filter(is_visible=True)
.order_by("start_time") .order_by("start_time")
) )
if self.request.organization:
visible_contests = visible_contests.filter(
is_organization_private=True, organizations=self.request.organization
)
context["current_contests"] = visible_contests.filter( context["current_contests"] = visible_contests.filter(
start_time__lte=now, end_time__gt=now start_time__lte=now, end_time__gt=now
@ -84,20 +66,26 @@ class FeedView(ListView):
context[ context[
"recent_organizations" "recent_organizations"
] = OrganizationProfile.get_most_recent_organizations(self.request.profile) ] = OrganizationProfile.get_most_recent_organizations(self.request.profile)
context["top_rated"] = Profile.objects.filter(is_unlisted=False).order_by(
profile_queryset = Profile.objects
if self.request.organization:
profile_queryset = self.request.organization.members
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by(
"-rating" "-rating"
)[:10] )[:10]
context["top_scorer"] = Profile.objects.filter(is_unlisted=False).order_by( context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by(
"-performance_points" "-performance_points"
)[:10] )[:10]
return context return context
class PostList(FeedView, PageVoteListView, BookMarkListView): class PostList(HomeFeedView):
model = BlogPost model = BlogPost
paginate_by = 10 paginate_by = 4
context_object_name = "posts" context_object_name = "posts"
feed_content_template_name = "blog/content.html"
url_name = "blog_post_list"
def get_queryset(self): def get_queryset(self):
queryset = ( queryset = (
@ -108,6 +96,8 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
filter = Q(is_organization_private=False) filter = Q(is_organization_private=False)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
filter |= Q(organizations__in=self.request.profile.organizations.all()) filter |= Q(organizations__in=self.request.profile.organizations.all())
if self.request.organization:
filter &= Q(organizations=self.request.organization)
queryset = queryset.filter(filter) queryset = queryset.filter(filter)
return queryset return queryset
@ -116,30 +106,18 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
context["title"] = ( context["title"] = (
self.title or _("Page %d of Posts") % context["page_obj"].number self.title or _("Page %d of Posts") % context["page_obj"].number
) )
context["first_page_href"] = reverse("home")
context["page_prefix"] = reverse("blog_post_list")
context["page_type"] = "blog" context["page_type"] = "blog"
context["post_comment_counts"] = {
int(page[2:]): count
for page, count in Comment.objects.filter(
page__in=["b:%d" % post.id for post in context["posts"]], hidden=False
)
.values_list("page")
.annotate(count=Count("page"))
.order_by()
}
context = self.add_pagevote_context_data(context)
context = self.add_bookmark_context_data(context)
return context return context
def get_comment_page(self, post): def get_comment_page(self, post):
return "b:%s" % post.id return "b:%s" % post.id
class TicketFeed(FeedView): class TicketFeed(HomeFeedView):
model = Ticket model = Ticket
context_object_name = "tickets" context_object_name = "tickets"
paginate_by = 30 paginate_by = 8
feed_content_template_name = "ticket/feed.html"
def get_queryset(self, is_own=True): def get_queryset(self, is_own=True):
profile = self.request.profile profile = self.request.profile
@ -171,28 +149,25 @@ class TicketFeed(FeedView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(TicketFeed, self).get_context_data(**kwargs) context = super(TicketFeed, self).get_context_data(**kwargs)
context["page_type"] = "ticket" context["page_type"] = "ticket"
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["title"] = _("Ticket feed") context["title"] = _("Ticket feed")
return context return context
class CommentFeed(FeedView): class CommentFeed(HomeFeedView):
model = Comment model = Comment
context_object_name = "comments" context_object_name = "comments"
paginate_by = 50 paginate_by = 15
feed_content_template_name = "comments/feed.html"
def get_queryset(self): def get_queryset(self):
return Comment.most_recent(self.request.user, 1000) return Comment.most_recent(
self.request.user, 100, organization=self.request.organization
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CommentFeed, self).get_context_data(**kwargs) context = super(CommentFeed, self).get_context_data(**kwargs)
context["page_type"] = "comment"
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["title"] = _("Comment feed") context["title"] = _("Comment feed")
context["page_type"] = "comment"
return context return context

View file

@ -74,13 +74,3 @@ class BookMarkDetailView(TemplateResponseMixin, SingleObjectMixin, View):
queryset = BookMark.objects.get_or_create(page=self.get_comment_page()) queryset = BookMark.objects.get_or_create(page=self.get_comment_page())
context["bookmark"] = queryset[0] context["bookmark"] = queryset[0]
return context return context
class BookMarkListView:
def add_bookmark_context_data(self, context, obj_list="object_list"):
for item in context[obj_list]:
bookmark, _ = BookMark.objects.get_or_create(
page=self.get_comment_page(item)
)
setattr(item, "bookmark", bookmark)
return context

View file

@ -149,15 +149,21 @@ class ContestList(
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.contest_query = None self.contest_query = None
self.org_query = [] self.org_query = []
self.show_orgs = 0
if request.GET.get("show_orgs"):
self.show_orgs = 1
if "orgs" in self.request.GET and self.request.profile: if "orgs" in self.request.GET and self.request.profile:
try: try:
self.org_query = list(map(int, request.GET.getlist("orgs"))) self.org_query = list(map(int, request.GET.getlist("orgs")))
if not self.request.user.is_superuser:
self.org_query = [ self.org_query = [
i i
for i in self.org_query for i in self.org_query
if i if i
in self.request.profile.organizations.values_list("id", flat=True) in self.request.profile.organizations.values_list(
"id", flat=True
)
] ]
except ValueError: except ValueError:
pass pass
@ -179,6 +185,10 @@ class ContestList(
queryset = queryset.filter( queryset = queryset.filter(
Q(key__icontains=query) | Q(name__icontains=query) Q(key__icontains=query) | Q(name__icontains=query)
) )
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
if self.show_orgs:
queryset = queryset.filter(organizations=None)
if self.org_query: if self.org_query:
queryset = queryset.filter(organizations__in=self.org_query) queryset = queryset.filter(organizations__in=self.org_query)
@ -225,7 +235,11 @@ class ContestList(
context["first_page_href"] = "." context["first_page_href"] = "."
context["contest_query"] = self.contest_query context["contest_query"] = self.contest_query
context["org_query"] = self.org_query context["org_query"] = self.org_query
context["show_orgs"] = int(self.show_orgs)
if self.request.profile: if self.request.profile:
if self.request.user.is_superuser:
context["organizations"] = Organization.objects.all()
else:
context["organizations"] = self.request.profile.organizations.all() context["organizations"] = self.request.profile.organizations.all()
context["page_type"] = "list" context["page_type"] = "list"
context.update(self.get_sort_context()) context.update(self.get_sort_context())
@ -404,6 +418,15 @@ class ContestDetail(
def get_title(self): def get_title(self):
return self.object.name return self.object.name
def get_editable_organizations(self):
if not self.request.profile:
return []
res = []
for organization in self.object.organizations.all():
if self.request.profile.can_edit_organization(organization):
res.append(organization)
return res
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ContestDetail, self).get_context_data(**kwargs) context = super(ContestDetail, self).get_context_data(**kwargs)
context["contest_problems"] = ( context["contest_problems"] = (
@ -421,6 +444,7 @@ class ContestDetail(
) )
.add_i18n_name(self.request.LANGUAGE_CODE) .add_i18n_name(self.request.LANGUAGE_CODE)
) )
context["editable_organizations"] = self.get_editable_organizations()
return context return context
@ -1002,8 +1026,8 @@ def contest_ranking_ajax(request, contest, participation=None):
queryset = contest.users.filter(virtual__gte=0) queryset = contest.users.filter(virtual__gte=0)
if request.GET.get("friend") == "true" and request.profile: if request.GET.get("friend") == "true" and request.profile:
friends = list(request.profile.get_friends()) friends = request.profile.get_friends()
queryset = queryset.filter(user__user__username__in=friends) queryset = queryset.filter(user_id__in=friends)
if request.GET.get("virtual") != "true": if request.GET.get("virtual") != "true":
queryset = queryset.filter(virtual=0) queryset = queryset.filter(virtual=0)
@ -1085,9 +1109,8 @@ class ContestFinalRanking(LoginRequiredMixin, ContestRanking):
def get_ranking_list(self): def get_ranking_list(self):
if not self.object.is_editable_by(self.request.user): if not self.object.is_editable_by(self.request.user):
raise Http404() raise Http404()
if self.object.format.has_hidden_subtasks: if not self.object.format.has_hidden_subtasks:
raise Http404() raise Http404()
return get_contest_ranking_list(self.request, self.object, show_final=True) return get_contest_ranking_list(self.request, self.object, show_final=True)

214
judge/views/course.py Normal file
View file

@ -0,0 +1,214 @@
from django.forms import ModelForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import UpdateView
from judge.models.course import Course, CourseResource
from django.views.generic import ListView, UpdateView, DetailView
from judge.views.feed import FeedView
from django.http import (
Http404,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
)
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from judge.utils.views import (
generic_message,
)
from django.urls import reverse_lazy
from django.contrib import messages
__all__ = [
"CourseList",
"CourseDetail",
"CourseResource",
"CourseResourceDetail",
"CourseStudentResults",
"CourseEdit",
"CourseResourceDetailEdit",
"CourseResourceEdit",
]
class CourseBase(object):
def is_editable_by(self, course=None):
if course is None:
course = self.object
if self.request.profile:
return Course.is_editable_by(course, self.request.profile)
return False
def is_accessible_by(self, course):
if course is None:
course = self.object
if self.request.profile:
return Course.is_accessible_by(course, self.request.profile)
return False
class CourseMixin(CourseBase):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_edit"] = self.is_editable_by(self.course)
context["can access"] = self.is_accessible_by(self.course)
context["course"] = self.course
return context
def dispatch(self, request, *args, **kwargs):
print(self)
try:
self.course_id = int(kwargs["pk"])
self.course = get_object_or_404(Course, id=self.course_id)
except Http404:
key = None
if hasattr(self, "slug_url_kwarg"):
key = kwargs.get(self.slug_url_kwarg, None)
if key:
return generic_message(
request,
_("No such course"),
_('Could not find a course with the key "%s".') % key,
)
else:
return generic_message(
request,
_("No such course"),
_("Could not find such course."),
)
if self.course.slug != kwargs["slug"]:
return HttpResponsePermanentRedirect(
request.get_full_path().replace(kwargs["slug"], self.course.slug)
)
return super(CourseMixin, self).dispatch(request, *args, **kwargs)
class CourseHomeView(CourseMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not hasattr(self, "course"):
self.course = self.object
return context
class CourseHome(CourseHomeView, FeedView):
template_name = "course/home.html"
def get_queryset(self):
return CourseResource.objects.filter(
is_public=True,
course=self.course,
).order_by("order")
def get_context_data(self, **kwargs):
context = super(CourseHome, self).get_context_data(**kwargs)
context["title"] = self.course.name
context["description"] = self.course.about
return context
class CourseResourceList(CourseMixin, ListView):
template_name = "course/resource.html"
def get_queryset(self):
return CourseResource.objects.filter(
is_public=True,
course=self.course,
).order_by("order")
def get_context_data(self, **kwargs):
context = super(CourseResourceList, self).get_context_data(**kwargs)
context["title"] = self.course.name
return context
class CourseResouceDetail(DetailView):
template_name = "course/resource-content.html"
model = CourseResource
def get_context_data(self, **kwargs):
context = super(CourseResouceDetail, self).get_context_data(**kwargs)
return context
class CourseAdminMixin(CourseMixin):
def dispatch(self, request, *args, **kwargs):
res = super(CourseAdminMixin, self).dispatch(request, *args, **kwargs)
if not hasattr(self, "course") or self.is_editable_by(self.course):
return res
return generic_message(
request,
_("Can't edit course"),
_("You are not allowed to edit this course."),
status=403,
)
class CourseResourceDetailEditForm(ModelForm):
def __init__(self, *args, **kwargs):
super(CourseResourceDetailEditForm, self).__init__(*args, **kwargs)
class CourseResourceDetailEdit(LoginRequiredMixin, UpdateView):
template_name = "course/resource_detail_edit.html"
model = CourseResource
fields = ["description", "files", "is_public"]
def get_success_url(self):
return self.request.get_full_path()
def form_valid(self, form):
form.save()
return super().form_valid(form)
class CourseResourceEdit(CourseMixin, LoginRequiredMixin, ListView):
template_name = "course/resource_edit.html"
def get_queryset(self):
return CourseResource.objects.filter(
course=self.course,
).order_by("order")
def post(self, request, *args, **kwargs):
queryset = self.get_queryset()
for resource in queryset:
if request.POST.get("resource-" + str(resource.pk) + "-delete") != None:
resource.delete()
else:
if request.POST.get("resource-" + str(resource.pk) + "-public") != None:
resource.is_public = True
else:
resource.is_public = False
resource.order = request.POST.get(
"resource-" + str(resource.pk) + "-order"
)
resource.save()
return HttpResponseRedirect(request.path)
def get_context_data(self, **kwargs):
return super(CourseResourceEdit, self).get_context_data(**kwargs)
class CourseListMixin(object):
def get_queryset(self):
return Course.objects.filter(is_open="true").values()
class CourseList(ListView):
model = Course
template_name = "course/list.html"
queryset = Course.objects.filter(is_public=True).filter(is_open=True)
def get_context_data(self, **kwargs):
context = super(CourseList, self).get_context_data(**kwargs)
available, enrolling = [], []
for course in Course.objects.filter(is_public=True).filter(is_open=True):
if Course.is_accessible_by(course, self.request.profile):
enrolling.append(course)
else:
available.append(course)
context["available"] = available
context["enrolling"] = enrolling
return context

34
judge/views/feed.py Normal file
View file

@ -0,0 +1,34 @@
from django.views.generic import ListView
from django.shortcuts import render
from django.urls import reverse
from judge.utils.infinite_paginator import InfinitePaginationMixin
class FeedView(InfinitePaginationMixin, ListView):
def get_feed_context(selfl, object_list):
return {}
def get(self, request, *args, **kwargs):
only_content = request.GET.get("only_content", None)
if only_content and self.feed_content_template_name:
queryset = self.get_queryset()
paginator, page, object_list, _ = self.paginate_queryset(
queryset, self.paginate_by
)
context = {
self.context_object_name: object_list,
"has_next_page": page.has_next(),
}
context.update(self.get_feed_context(object_list))
return render(request, self.feed_content_template_name, context)
return super(FeedView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
context["feed_content_url"] = reverse(self.url_name)
except Exception as e:
context["feed_content_url"] = self.request.path
return context

View file

@ -42,7 +42,6 @@ class NotificationList(ListView):
context["unseen_count"] = self.unseen_cnt context["unseen_count"] = self.unseen_cnt
context["title"] = _("Notifications (%d unseen)" % context["unseen_count"]) context["title"] = _("Notifications (%d unseen)" % context["unseen_count"])
context["has_notifications"] = self.queryset.exists() context["has_notifications"] = self.queryset.exists()
context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page))
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View file

@ -35,6 +35,7 @@ from django.views.generic.detail import (
SingleObjectTemplateResponseMixin, SingleObjectTemplateResponseMixin,
) )
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.contrib.sites.shortcuts import get_current_site
from reversion import revisions from reversion import revisions
from judge.forms import ( from judge.forms import (
@ -71,8 +72,7 @@ from judge.utils.problems import user_attempted_ids, user_completed_ids
from judge.views.problem import ProblemList from judge.views.problem import ProblemList
from judge.views.contests import ContestList from judge.views.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.pagevote import PageVoteListView from judge.views.feed import FeedView
from judge.views.bookmark import BookMarkListView
__all__ = [ __all__ = [
"OrganizationList", "OrganizationList",
@ -96,14 +96,9 @@ class OrganizationBase(object):
def can_edit_organization(self, org=None): def can_edit_organization(self, org=None):
if org is None: if org is None:
org = self.object org = self.object
if not self.request.user.is_authenticated: if self.request.profile:
return self.request.profile.can_edit_organization(org)
return False return False
profile_id = self.request.profile.id
return (
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id
or self.request.user.is_superuser
)
def is_member(self, org=None): def is_member(self, org=None):
if org is None: if org is None:
@ -112,6 +107,13 @@ class OrganizationBase(object):
self.request.profile in org if self.request.user.is_authenticated else False self.request.profile in org if self.request.user.is_authenticated else False
) )
def is_admin(self, org=None):
if org is None:
org = self.object
if self.request.profile:
return org.admins.filter(id=self.request.profile.id).exists()
return False
def can_access(self, org): def can_access(self, org):
if self.request.user.is_superuser: if self.request.user.is_superuser:
return True return True
@ -124,6 +126,7 @@ class OrganizationMixin(OrganizationBase):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["is_member"] = self.is_member(self.organization) context["is_member"] = self.is_member(self.organization)
context["is_admin"] = self.is_admin(self.organization)
context["can_edit"] = self.can_edit_organization(self.organization) context["can_edit"] = self.can_edit_organization(self.organization)
context["organization"] = self.organization context["organization"] = self.organization
context["logo_override_image"] = self.organization.logo_override_image context["logo_override_image"] = self.organization.logo_override_image
@ -136,6 +139,8 @@ class OrganizationMixin(OrganizationBase):
self.organization_id = int(kwargs["pk"]) self.organization_id = int(kwargs["pk"])
self.organization = get_object_or_404(Organization, id=self.organization_id) self.organization = get_object_or_404(Organization, id=self.organization_id)
except Http404: except Http404:
key = None
if hasattr(self, "slug_url_kwarg"):
key = kwargs.get(self.slug_url_kwarg, None) key = kwargs.get(self.slug_url_kwarg, None)
if key: if key:
return generic_message( return generic_message(
@ -189,7 +194,7 @@ class MemberOrganizationMixin(OrganizationMixin):
) )
class OrganizationHomeViewContext: class OrganizationHomeView(OrganizationMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if not hasattr(self, "organization"): if not hasattr(self, "organization"):
@ -216,28 +221,6 @@ class OrganizationHomeViewContext:
return context return context
class OrganizationDetailView(
OrganizationMixin, OrganizationHomeViewContext, DetailView
):
context_object_name = "organization"
model = Organization
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.slug != kwargs["slug"]:
return HttpResponsePermanentRedirect(
request.get_full_path().replace(kwargs["slug"], self.object.slug)
)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_edit"] = self.can_edit_organization()
context["is_member"] = self.is_member()
return context
class OrganizationList(TitleMixin, ListView, OrganizationBase): class OrganizationList(TitleMixin, ListView, OrganizationBase):
model = Organization model = Organization
context_object_name = "organizations" context_object_name = "organizations"
@ -267,58 +250,46 @@ class OrganizationList(TitleMixin, ListView, OrganizationBase):
return context return context
class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListView): class OrganizationHome(OrganizationHomeView, FeedView):
template_name = "organization/home.html" template_name = "organization/home.html"
pagevote_object_name = "posts" paginate_by = 4
context_object_name = "posts"
feed_content_template_name = "blog/content.html"
def get_posts_and_page_obj(self): def get_queryset(self):
posts = ( return (
BlogPost.objects.filter( BlogPost.objects.filter(
visible=True, visible=True,
publish_on__lte=timezone.now(), publish_on__lte=timezone.now(),
is_organization_private=True, is_organization_private=True,
organizations=self.object, organizations=self.organization,
) )
.order_by("-sticky", "-publish_on") .order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations") .prefetch_related("authors__user", "organizations")
) )
paginator = Paginator(posts, 10)
page_number = self.request.GET.get("page", 1)
posts = paginator.get_page(page_number)
return posts, paginator.page(page_number)
def get_comment_page(self, post): def get_comment_page(self, post):
return "b:%s" % post.id return "b:%s" % post.id
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs) context = super(OrganizationHome, self).get_context_data(**kwargs)
context["title"] = self.object.name context["title"] = self.organization.name
context["posts"], context["page_obj"] = self.get_posts_and_page_obj() http = "http" if settings.DMOJ_SSL == 0 else "https"
context = self.add_pagevote_context_data(context, "posts") context["organization_subdomain"] = (
context = self.add_bookmark_context_data(context, "posts") http
+ "://"
# Hack: This allows page_obj to have page_range for non-ListView class + self.organization.slug
setattr( + "."
context["page_obj"], "page_range", context["posts"].paginator.page_range + get_current_site(self.request).domain
) )
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["post_comment_counts"] = {
int(page[2:]): count
for page, count in Comment.objects.filter(
page__in=["b:%d" % post.id for post in context["posts"]], hidden=False
)
.values_list("page")
.annotate(count=Count("page"))
.order_by()
}
now = timezone.now() now = timezone.now()
visible_contests = ( visible_contests = (
Contest.get_visible_contests(self.request.user) Contest.get_visible_contests(self.request.user)
.filter( .filter(
is_visible=True, is_organization_private=True, organizations=self.object is_visible=True,
is_organization_private=True,
organizations=self.organization,
) )
.order_by("start_time") .order_by("start_time")
) )
@ -331,11 +302,27 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListVie
return context return context
class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView): class OrganizationUsers(QueryStringSortMixin, OrganizationMixin, FeedView):
template_name = "organization/users.html" template_name = "organization/users.html"
all_sorts = frozenset(("points", "problem_count", "rating", "performance_points")) all_sorts = frozenset(("points", "problem_count", "rating", "performance_points"))
default_desc = all_sorts default_desc = all_sorts
default_sort = "-performance_points" default_sort = "-performance_points"
context_object_name = "users"
def get_queryset(self):
return ranker(
self.organization.members.filter(is_unlisted=False)
.order_by(self.order, "id")
.select_related("user")
.only(
"display_rank",
"user__username",
"points",
"rating",
"performance_points",
"problem_count",
)
)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs) res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs)
@ -350,26 +337,13 @@ class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationUsers, self).get_context_data(**kwargs) context = super(OrganizationUsers, self).get_context_data(**kwargs)
context["title"] = _("%s Members") % self.object.name context["title"] = _("%s Members") % self.organization.name
context["partial"] = True context["partial"] = True
context["kick_url"] = reverse( context["kick_url"] = reverse(
"organization_user_kick", args=[self.object.id, self.object.slug] "organization_user_kick",
args=[self.organization.id, self.organization.slug],
) )
context["users"] = ranker(
self.get_object()
.members.filter(is_unlisted=False)
.order_by(self.order, "id")
.select_related("user")
.only(
"display_rank",
"user__username",
"points",
"rating",
"performance_points",
"problem_count",
)
)
context["first_page_href"] = "." context["first_page_href"] = "."
context["page_type"] = "users" context["page_type"] = "users"
context.update(self.get_sort_context()) context.update(self.get_sort_context())
@ -378,6 +352,7 @@ class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemList): class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemList):
template_name = "organization/problems.html" template_name = "organization/problems.html"
filter_organization = True
def get_queryset(self): def get_queryset(self):
self.org_query = [self.organization_id] self.org_query = [self.organization_id]
@ -387,17 +362,6 @@ class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemL
self.setup_problem_list(request) self.setup_problem_list(request)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_latest_attempted_problems(self, limit=None):
if not self.profile:
return ()
problems = set(self.get_queryset().values_list("code", flat=True))
result = list(user_attempted_ids(self.profile).values())
result = [i for i in result if i["code"] in problems]
result = sorted(result, key=lambda d: -d["last_submission"])
if limit:
result = result[:limit]
return result
def get_completed_problems(self): def get_completed_problems(self):
return user_completed_ids(self.profile) if self.profile is not None else () return user_completed_ids(self.profile) if self.profile is not None else ()
@ -418,8 +382,7 @@ class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemL
class OrganizationContestMixin( class OrganizationContestMixin(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationMixin, OrganizationHomeView,
OrganizationHomeViewContext,
): ):
model = Contest model = Contest
@ -477,7 +440,7 @@ class OrganizationSubmissions(
def contest(self): def contest(self):
return None return None
def _get_queryset(self): def get_queryset(self):
return ( return (
super() super()
._get_entire_queryset() ._get_entire_queryset()
@ -610,8 +573,7 @@ class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
class OrganizationRequestDetail( class OrganizationRequestDetail(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationMixin, OrganizationHomeView,
OrganizationHomeViewContext,
DetailView, DetailView,
): ):
model = OrganizationRequest model = OrganizationRequest
@ -636,7 +598,8 @@ OrganizationRequestFormSet = modelformset_factory(
class OrganizationRequestBaseView( class OrganizationRequestBaseView(
OrganizationDetailView, DetailView,
OrganizationHomeView,
TitleMixin, TitleMixin,
LoginRequiredMixin, LoginRequiredMixin,
SingleObjectTemplateResponseMixin, SingleObjectTemplateResponseMixin,
@ -757,7 +720,7 @@ class AddOrganizationMember(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
AdminOrganizationMixin, AdminOrganizationMixin,
OrganizationDetailView, OrganizationHomeView,
UpdateView, UpdateView,
): ):
template_name = "organization/add-member.html" template_name = "organization/add-member.html"
@ -819,7 +782,7 @@ class EditOrganization(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
AdminOrganizationMixin, AdminOrganizationMixin,
OrganizationDetailView, OrganizationHomeView,
UpdateView, UpdateView,
): ):
template_name = "organization/edit.html" template_name = "organization/edit.html"
@ -1020,7 +983,7 @@ class EditOrganizationContest(
class AddOrganizationBlog( class AddOrganizationBlog(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationHomeViewContext, OrganizationHomeView,
MemberOrganizationMixin, MemberOrganizationMixin,
CreateView, CreateView,
): ):
@ -1071,7 +1034,7 @@ class AddOrganizationBlog(
class EditOrganizationBlog( class EditOrganizationBlog(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationHomeViewContext, OrganizationHomeView,
MemberOrganizationMixin, MemberOrganizationMixin,
UpdateView, UpdateView,
): ):
@ -1165,7 +1128,7 @@ class PendingBlogs(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
MemberOrganizationMixin, MemberOrganizationMixin,
OrganizationHomeViewContext, OrganizationHomeView,
ListView, ListView,
): ):
model = BlogPost model = BlogPost

View file

@ -104,13 +104,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
queryset = PageVote.objects.get_or_create(page=self.get_comment_page()) queryset = PageVote.objects.get_or_create(page=self.get_comment_page())
context["pagevote"] = queryset[0] context["pagevote"] = queryset[0]
return context return context
class PageVoteListView:
def add_pagevote_context_data(self, context, obj_list="object_list"):
for item in context[obj_list]:
pagevote, _ = PageVote.objects.get_or_create(
page=self.get_comment_page(item)
)
setattr(item, "pagevote", pagevote)
return context

View file

@ -63,7 +63,6 @@ from judge.models import (
Submission, Submission,
SubmissionSource, SubmissionSource,
Organization, Organization,
VolunteerProblemVote,
Profile, Profile,
LanguageTemplate, LanguageTemplate,
) )
@ -76,6 +75,7 @@ from judge.utils.problems import (
hot_problems, hot_problems,
user_attempted_ids, user_attempted_ids,
user_completed_ids, user_completed_ids,
get_related_problems,
) )
from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter from judge.utils.tickets import own_ticket_filter
@ -86,8 +86,9 @@ from judge.utils.views import (
generic_message, generic_message,
) )
from judge.ml.collab_filter import CollabFilter from judge.ml.collab_filter import CollabFilter
from judge.views.pagevote import PageVoteDetailView, PageVoteListView from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView from judge.views.bookmark import BookMarkDetailView
from judge.views.feed import FeedView
def get_contest_problem(problem, profile): def get_contest_problem(problem, profile):
@ -105,6 +106,14 @@ def get_contest_submission_count(problem, profile, virtual):
) )
def get_problems_in_organization(request, organization):
problem_list = ProblemList(request=request)
problem_list.setup_problem_list(request)
problem_list.org_query = [organization.id]
problems = problem_list.get_normal_queryset()
return problems
class ProblemMixin(object): class ProblemMixin(object):
model = Problem model = Problem
slug_url_kwarg = "problem" slug_url_kwarg = "problem"
@ -145,10 +154,13 @@ class SolvedProblemMixin(object):
else: else:
return user_attempted_ids(self.profile) if self.profile is not None else () return user_attempted_ids(self.profile) if self.profile is not None else ()
def get_latest_attempted_problems(self, limit=None): def get_latest_attempted_problems(self, limit=None, queryset=None):
if self.in_contest or not self.profile: if self.in_contest or not self.profile:
return () return ()
result = list(user_attempted_ids(self.profile).values()) result = list(user_attempted_ids(self.profile).values())
if queryset:
queryset_ids = set([i.code for i in queryset])
result = filter(lambda i: i["code"] in queryset_ids, result)
result = sorted(result, key=lambda d: -d["last_submission"]) result = sorted(result, key=lambda d: -d["last_submission"])
if limit: if limit:
result = result[:limit] result = result[:limit]
@ -185,31 +197,34 @@ class ProblemSolution(
template_name = "problem/editorial.html" template_name = "problem/editorial.html"
def get_title(self): def get_title(self):
return _("Editorial for {0}").format(self.object.name) return _("Editorial for {0}").format(self.problem.name)
def get_content_title(self): def get_content_title(self):
return format_html( return format_html(
_('Editorial for <a href="{1}">{0}</a>'), _('Editorial for <a href="{1}">{0}</a>'),
self.object.name, self.problem.name,
reverse("problem_detail", args=[self.object.code]), reverse("problem_detail", args=[self.problem.code]),
) )
def get_object(self):
self.problem = super().get_object()
solution = get_object_or_404(Solution, problem=self.problem)
return solution
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProblemSolution, self).get_context_data(**kwargs) context = super(ProblemSolution, self).get_context_data(**kwargs)
solution = self.get_object()
solution = get_object_or_404(Solution, problem=self.object)
if ( if (
not solution.is_public or solution.publish_on > timezone.now() not solution.is_public or solution.publish_on > timezone.now()
) and not self.request.user.has_perm("judge.see_private_solution"): ) and not self.request.user.has_perm("judge.see_private_solution"):
raise Http404() raise Http404()
context["solution"] = solution context["solution"] = solution
context["has_solved_problem"] = self.object.id in self.get_completed_problems() context["has_solved_problem"] = self.problem.id in self.get_completed_problems()
return context return context
def get_comment_page(self): def get_comment_page(self):
return "s:" + self.object.code return "s:" + self.problem.code
class ProblemRaw( class ProblemRaw(
@ -341,6 +356,10 @@ class ProblemDetail(
else: else:
context["fileio_input"] = None context["fileio_input"] = None
context["fileio_output"] = None context["fileio_output"] = None
if not self.in_contest:
context["related_problems"] = get_related_problems(
self.profile, self.object
)
return context return context
@ -454,6 +473,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
default_desc = frozenset(("date", "points", "ac_rate", "user_count")) default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
default_sort = "-date" default_sort = "-date"
first_page_href = None first_page_href = None
filter_organization = False
def get_paginator( def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
@ -465,13 +485,10 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
padding=2, padding=2,
orphans=orphans, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, allow_empty_first_page=allow_empty_first_page,
count=queryset.values("pk").count() if not self.in_contest else None,
**kwargs **kwargs
) )
if not self.in_contest: if not self.in_contest:
# Get the number of pages and then add in this magic.
# noinspection PyStatementEffect
paginator.num_pages
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
sort_key = self.order.lstrip("-") sort_key = self.order.lstrip("-")
if sort_key in self.sql_sort: if sort_key in self.sql_sort:
@ -573,25 +590,13 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
] ]
def get_normal_queryset(self): def get_normal_queryset(self):
filter = Q(is_public=True) queryset = Problem.get_visible_problems(self.request.user)
if self.profile is not None: queryset = queryset.select_related("group")
filter |= Q(authors=self.profile)
filter |= Q(curators=self.profile)
filter |= Q(testers=self.profile)
queryset = (
Problem.objects.filter(filter).select_related("group").defer("description")
)
if not self.request.user.has_perm("see_organization_problem"):
filter = Q(is_organization_private=False)
if self.profile is not None:
filter |= Q(organizations__in=self.profile.organizations.all())
queryset = queryset.filter(filter)
if self.profile is not None and self.hide_solved: if self.profile is not None and self.hide_solved:
queryset = queryset.exclude( solved_problems = self.get_completed_problems()
id__in=Submission.objects.filter( queryset = queryset.exclude(id__in=solved_problems)
user=self.profile, points=F("problem__points") if not self.org_query and self.request.organization:
).values_list("problem__id", flat=True) self.org_query = [self.request.organization.id]
)
if self.org_query: if self.org_query:
self.org_query = self.get_org_query(self.org_query) self.org_query = self.get_org_query(self.org_query)
queryset = queryset.filter( queryset = queryset.filter(
@ -652,6 +657,8 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProblemList, self).get_context_data(**kwargs) context = super(ProblemList, self).get_context_data(**kwargs)
if self.request.organization:
self.filter_organization = True
context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved) context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved)
context["show_types"] = 0 if self.in_contest else int(self.show_types) context["show_types"] = 0 if self.in_contest else int(self.show_types)
context["full_text"] = 0 if self.in_contest else int(self.full_text) context["full_text"] = 0 if self.in_contest else int(self.full_text)
@ -663,8 +670,12 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
if self.request.profile: if self.request.profile:
context["organizations"] = self.request.profile.organizations.all() context["organizations"] = self.request.profile.organizations.all()
all_authors_ids = set(Problem.objects.values_list("authors", flat=True)) all_authors_ids = Problem.objects.values_list("authors", flat=True)
context["all_authors"] = Profile.objects.filter(id__in=all_authors_ids) context["all_authors"] = (
Profile.objects.filter(id__in=all_authors_ids)
.select_related("user")
.values("id", "user__username")
)
context["category"] = self.category context["category"] = self.category
context["categories"] = ProblemGroup.objects.all() context["categories"] = ProblemGroup.objects.all()
if self.show_types: if self.show_types:
@ -676,7 +687,9 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
context["search_query"] = self.search_query context["search_query"] = self.search_query
context["completed_problem_ids"] = self.get_completed_problems() context["completed_problem_ids"] = self.get_completed_problems()
context["attempted_problems"] = self.get_attempted_problems() context["attempted_problems"] = self.get_attempted_problems()
context["last_attempted_problems"] = self.get_latest_attempted_problems(15) context["last_attempted_problems"] = self.get_latest_attempted_problems(
15, context["problems"] if self.filter_organization else None
)
context["page_type"] = "list" context["page_type"] = "list"
context.update(self.get_sort_paginate_context()) context.update(self.get_sort_paginate_context())
if not self.in_contest: if not self.in_contest:
@ -751,7 +764,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
) )
def GET_with_session(self, request, key): def GET_with_session(self, request, key):
if not request.GET: if not request.GET.get(key):
return request.session.get(key, False) return request.session.get(key, False)
return request.GET.get(key, None) == "1" return request.GET.get(key, None) == "1"
@ -820,32 +833,15 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
return HttpResponseRedirect(request.get_full_path()) return HttpResponseRedirect(request.get_full_path())
class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView): class ProblemFeed(ProblemList, FeedView):
model = Problem model = Problem
context_object_name = "problems" context_object_name = "problems"
template_name = "problem/feed.html" template_name = "problem/feed.html"
paginate_by = 20 feed_content_template_name = "problem/feed/problems.html"
paginate_by = 4
title = _("Problem feed") title = _("Problem feed")
feed_type = None feed_type = None
def GET_with_session(self, request, key):
if not request.GET:
return request.session.get(key, key == "hide_solved")
return request.GET.get(key, None) == "1"
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
return DiggPaginator(
queryset,
per_page,
body=6,
padding=2,
orphans=orphans,
allow_empty_first_page=allow_empty_first_page,
**kwargs
)
def get_comment_page(self, problem): def get_comment_page(self, problem):
return "p:%s" % problem.code return "p:%s" % problem.code
@ -945,6 +941,12 @@ class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
res[position_in_q[problem.id]] = problem res[position_in_q[problem.id]] = problem
return res return res
def get_feed_context(self, object_list):
return {
"completed_problem_ids": self.get_completed_problems(),
"attempted_problems": self.get_attempted_problems(),
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProblemFeed, self).get_context_data(**kwargs) context = super(ProblemFeed, self).get_context_data(**kwargs)
context["page_type"] = "feed" context["page_type"] = "feed"
@ -952,8 +954,6 @@ class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
context["feed_type"] = self.feed_type context["feed_type"] = self.feed_type
context["has_show_editorial_option"] = False context["has_show_editorial_option"] = False
context["has_have_editorial_option"] = False context["has_have_editorial_option"] = False
context = self.add_pagevote_context_data(context)
context = self.add_bookmark_context_data(context)
return context return context

View file

@ -7,7 +7,7 @@ from judge.utils.problems import get_result_data
from judge.utils.raw_sql import join_sql_subquery from judge.utils.raw_sql import join_sql_subquery
from judge.views.submission import ForceContestMixin, ProblemSubmissions from judge.views.submission import ForceContestMixin, ProblemSubmissions
__all__ = ["RankedSubmissions", "ContestRankedSubmission"] __all__ = ["RankedSubmissions"]
class RankedSubmissions(ProblemSubmissions): class RankedSubmissions(ProblemSubmissions):
@ -27,7 +27,7 @@ class RankedSubmissions(ProblemSubmissions):
constraint = "" constraint = ""
queryset = ( queryset = (
super(RankedSubmissions, self) super(RankedSubmissions, self)
._get_queryset() .get_queryset()
.filter(user__is_unlisted=False) .filter(user__is_unlisted=False)
) )
join_sql_subquery( join_sql_subquery(
@ -76,43 +76,4 @@ class RankedSubmissions(ProblemSubmissions):
) )
def _get_result_data(self): def _get_result_data(self):
return get_result_data( return get_result_data(super(RankedSubmissions, self).get_queryset().order_by())
super(RankedSubmissions, self)._get_queryset().order_by()
)
class ContestRankedSubmission(ForceContestMixin, RankedSubmissions):
def get_title(self):
if self.problem.is_accessible_by(self.request.user):
return _("Best solutions for %(problem)s in %(contest)s") % {
"problem": self.problem_name,
"contest": self.contest.name,
}
return _("Best solutions for problem %(number)s in %(contest)s") % {
"number": self.get_problem_number(self.problem),
"contest": self.contest.name,
}
def get_content_title(self):
if self.problem.is_accessible_by(self.request.user):
return format_html(
_('Best solutions for <a href="{1}">{0}</a> in <a href="{3}">{2}</a>'),
self.problem_name,
reverse("problem_detail", args=[self.problem.code]),
self.contest.name,
reverse("contest_view", args=[self.contest.key]),
)
return format_html(
_('Best solutions for problem {0} in <a href="{2}">{1}</a>'),
self.get_problem_number(self.problem),
self.contest.name,
reverse("contest_view", args=[self.contest.key]),
)
def _get_result_data(self):
return get_result_data(
Submission.objects.filter(
problem_id=self.problem.id,
contest__participation__contest_id=self.contest.id,
)
)

View file

@ -41,19 +41,15 @@ from judge.models import ProblemTranslation
from judge.models import Profile from judge.models import Profile
from judge.models import Submission from judge.models import Submission
from judge.utils.problems import get_result_data from judge.utils.problems import get_result_data
from judge.utils.problems import user_authored_ids from judge.utils.problems import user_completed_ids, user_editable_ids, user_tester_ids
from judge.utils.problems import user_completed_ids
from judge.utils.problems import user_editable_ids
from judge.utils.problem_data import get_problem_case from judge.utils.problem_data import get_problem_case
from judge.utils.raw_sql import join_sql_subquery, use_straight_join from judge.utils.raw_sql import join_sql_subquery, use_straight_join
from judge.utils.views import DiggPaginatorMixin from judge.utils.views import DiggPaginatorMixin
from judge.utils.infinite_paginator import InfinitePaginationMixin
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
MAX_NUMBER_OF_QUERY_SUBMISSIONS = 50000
def submission_related(queryset): def submission_related(queryset):
return queryset.select_related("user__user", "problem", "language").only( return queryset.select_related("user__user", "problem", "language").only(
"id", "id",
@ -266,7 +262,7 @@ class SubmissionStatus(SubmissionDetailBase):
can_see_testcases = self.access_testcases_in_contest() can_see_testcases = self.access_testcases_in_contest()
if contest is not None: if contest is not None:
prefix_length = contest.problem.output_prefix_override prefix_length = contest.problem.output_prefix_override or 0
if contest is None or prefix_length > 0 or can_see_testcases: if contest is None or prefix_length > 0 or can_see_testcases:
context["cases_data"] = get_cases_data(submission) context["cases_data"] = get_cases_data(submission)
@ -333,7 +329,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
return result return result
def _get_result_data(self): def _get_result_data(self):
return get_result_data(self._get_queryset().order_by()) return get_result_data(self.get_queryset().order_by())
def access_check(self, request): def access_check(self, request):
pass pass
@ -404,16 +400,21 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
language__in=Language.objects.filter(key__in=self.selected_languages) language__in=Language.objects.filter(key__in=self.selected_languages)
) )
if self.selected_statuses: if self.selected_statuses:
queryset = queryset.filter( submission_results = [i for i, _ in Submission.RESULT]
Q(result__in=self.selected_statuses) if self.selected_statuses[0] in submission_results:
| Q(status__in=self.selected_statuses) queryset = queryset.filter(result__in=self.selected_statuses)
) else:
queryset = queryset.filter(status__in=self.selected_statuses)
return queryset return queryset
def _get_queryset(self): def get_queryset(self):
queryset = self._get_entire_queryset() queryset = self._get_entire_queryset()
if not self.in_contest: if not self.in_contest:
if self.request.organization:
queryset = queryset.filter(
contest_object__organizations=self.request.organization
)
join_sql_subquery( join_sql_subquery(
queryset, queryset,
subquery=str( subquery=str(
@ -429,12 +430,12 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
) )
return queryset return queryset
def get_queryset(self):
return self._get_queryset()[:MAX_NUMBER_OF_QUERY_SUBMISSIONS]
def get_my_submissions_page(self): def get_my_submissions_page(self):
return None return None
def get_friend_submissions_page(self):
return None
def get_all_submissions_page(self): def get_all_submissions_page(self):
return reverse("all_submissions") return reverse("all_submissions")
@ -473,12 +474,12 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
context["completed_problem_ids"] = ( context["completed_problem_ids"] = (
user_completed_ids(self.request.profile) if authenticated else [] user_completed_ids(self.request.profile) if authenticated else []
) )
context["authored_problem_ids"] = (
user_authored_ids(self.request.profile) if authenticated else []
)
context["editable_problem_ids"] = ( context["editable_problem_ids"] = (
user_editable_ids(self.request.profile) if authenticated else [] user_editable_ids(self.request.profile) if authenticated else []
) )
context["tester_problem_ids"] = (
user_tester_ids(self.request.profile) if authenticated else []
)
context["all_languages"] = Language.objects.all().values_list("key", "name") context["all_languages"] = Language.objects.all().values_list("key", "name")
context["selected_languages"] = self.selected_languages context["selected_languages"] = self.selected_languages
@ -499,6 +500,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
) )
context["first_page_href"] = (self.first_page_href or ".") + suffix context["first_page_href"] = (self.first_page_href or ".") + suffix
context["my_submissions_link"] = self.get_my_submissions_page() context["my_submissions_link"] = self.get_my_submissions_page()
context["friend_submissions_link"] = self.get_friend_submissions_page()
context["all_submissions_link"] = self.get_all_submissions_page() context["all_submissions_link"] = self.get_all_submissions_page()
context["page_type"] = self.page_type context["page_type"] = self.page_type
@ -513,8 +515,8 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
if check is not None: if check is not None:
return check return check
self.selected_languages = set(request.GET.getlist("language")) self.selected_languages = request.GET.getlist("language")
self.selected_statuses = set(request.GET.getlist("status")) self.selected_statuses = request.GET.getlist("status")
if self.in_contest and self.contest.is_editable_by(self.request.user): if self.in_contest and self.contest.is_editable_by(self.request.user):
self.include_frozen = True self.include_frozen = True
@ -554,11 +556,25 @@ class ConditionalUserTabMixin(object):
return context return context
class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase): class GeneralSubmissions(SubmissionsListBase):
def _get_queryset(self): def get_my_submissions_page(self):
if self.request.user.is_authenticated:
return reverse(
"all_user_submissions", kwargs={"user": self.request.user.username}
)
return None
def get_friend_submissions_page(self):
if self.request.user.is_authenticated:
return reverse("all_friend_submissions")
return None
class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, GeneralSubmissions):
def get_queryset(self):
return ( return (
super(AllUserSubmissions, self) super(AllUserSubmissions, self)
._get_queryset() .get_queryset()
.filter(user_id=self.profile.id) .filter(user_id=self.profile.id)
) )
@ -569,19 +585,13 @@ class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase
def get_content_title(self): def get_content_title(self):
if self.request.user.is_authenticated and self.request.profile == self.profile: if self.request.user.is_authenticated and self.request.profile == self.profile:
return format_html("All my submissions") return format_html(_("All my submissions"))
return format_html( return format_html(
'All submissions by <a href="{1}">{0}</a>', _('All submissions by <a href="{1}">{0}</a>'),
self.username, self.username,
reverse("user_page", args=[self.username]), reverse("user_page", args=[self.username]),
) )
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
return reverse(
"all_user_submissions", kwargs={"user": self.request.user.username}
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AllUserSubmissions, self).get_context_data(**kwargs) context = super(AllUserSubmissions, self).get_context_data(**kwargs)
context["dynamic_update"] = context["page_obj"].number == 1 context["dynamic_update"] = context["page_obj"].number == 1
@ -590,12 +600,29 @@ class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase
return context return context
class AllFriendSubmissions(LoginRequiredMixin, GeneralSubmissions):
def get_queryset(self):
friends = self.request.profile.get_friends()
return (
super(AllFriendSubmissions, self).get_queryset().filter(user_id__in=friends)
)
def get_title(self):
return _("All friend submissions")
def get_context_data(self, **kwargs):
context = super(AllFriendSubmissions, self).get_context_data(**kwargs)
context["dynamic_update"] = False
context["page_type"] = "friend_tab"
return context
class ProblemSubmissionsBase(SubmissionsListBase): class ProblemSubmissionsBase(SubmissionsListBase):
show_problem = False show_problem = False
dynamic_update = True dynamic_update = True
check_contest_in_access_check = False check_contest_in_access_check = False
def _get_queryset(self): def get_queryset(self):
if ( if (
self.in_contest self.in_contest
and not self.contest.contest_problems.filter( and not self.contest.contest_problems.filter(
@ -687,10 +714,10 @@ class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissi
if not self.is_own: if not self.is_own:
self.access_check_contest(request) self.access_check_contest(request)
def _get_queryset(self): def get_queryset(self):
return ( return (
super(UserProblemSubmissions, self) super(UserProblemSubmissions, self)
._get_queryset() .get_queryset()
.filter(user_id=self.profile.id) .filter(user_id=self.profile.id)
) )
@ -740,15 +767,15 @@ def single_submission(request, submission_id, show_problem=True):
"submission/row.html", "submission/row.html",
{ {
"submission": submission, "submission": submission,
"authored_problem_ids": user_authored_ids(request.profile)
if authenticated
else [],
"completed_problem_ids": user_completed_ids(request.profile) "completed_problem_ids": user_completed_ids(request.profile)
if authenticated if authenticated
else [], else [],
"editable_problem_ids": user_editable_ids(request.profile) "editable_problem_ids": user_editable_ids(request.profile)
if authenticated if authenticated
else [], else [],
"tester_problem_ids": user_tester_ids(request.profile)
if authenticated
else [],
"show_problem": show_problem, "show_problem": show_problem,
"problem_name": show_problem "problem_name": show_problem
and submission.problem.translated_name(request.LANGUAGE_CODE), and submission.problem.translated_name(request.LANGUAGE_CODE),
@ -768,31 +795,46 @@ def single_submission_query(request):
return single_submission(request, int(request.GET["id"]), bool(show_problem)) return single_submission(request, int(request.GET["id"]), bool(show_problem))
class AllSubmissions(SubmissionsListBase): class AllSubmissions(InfinitePaginationMixin, GeneralSubmissions):
stats_update_interval = 3600 stats_update_interval = 3600
def get_my_submissions_page(self): @property
if self.request.user.is_authenticated: def use_infinite_pagination(self):
return reverse( return not self.in_contest
"all_user_submissions", kwargs={"user": self.request.user.username}
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AllSubmissions, self).get_context_data(**kwargs) context = super(AllSubmissions, self).get_context_data(**kwargs)
context["dynamic_update"] = context["page_obj"].number == 1 context["dynamic_update"] = (
context["page_obj"].number == 1
) and not self.request.organization
context["last_msg"] = event.last() context["last_msg"] = event.last()
context["stats_update_interval"] = self.stats_update_interval context["stats_update_interval"] = self.stats_update_interval
return context return context
def _get_result_data(self): def _get_result_data(self):
if self.in_contest or self.selected_languages or self.selected_statuses: if self.request.organization or self.in_contest:
return super(AllSubmissions, self)._get_result_data() return super(AllSubmissions, self)._get_result_data()
key = "global_submission_result_data" key = "global_submission_result_data"
if self.selected_statuses:
key += ":" + ",".join(self.selected_statuses)
if self.selected_languages:
key += ":" + ",".join(self.selected_languages)
result = cache.get(key) result = cache.get(key)
if result: if result:
return result return result
result = super(AllSubmissions, self)._get_result_data() queryset = Submission.objects
if self.selected_languages:
queryset = queryset.filter(
language__in=Language.objects.filter(key__in=self.selected_languages)
)
if self.selected_statuses:
submission_results = [i for i, _ in Submission.RESULT]
if self.selected_statuses[0] in submission_results:
queryset = queryset.filter(result__in=self.selected_statuses)
else:
queryset = queryset.filter(status__in=self.selected_statuses)
result = get_result_data(queryset)
cache.set(key, result, self.stats_update_interval) cache.set(key, result, self.stats_update_interval)
return result return result

View file

@ -44,12 +44,12 @@ from judge.utils.problems import contest_completed_ids, user_completed_ids
from judge.utils.ranker import ranker from judge.utils.ranker import ranker
from judge.utils.unicode import utf8text from judge.utils.unicode import utf8text
from judge.utils.views import ( from judge.utils.views import (
DiggPaginatorMixin,
QueryStringSortMixin, QueryStringSortMixin,
TitleMixin, TitleMixin,
generic_message, generic_message,
SingleObjectFormView, SingleObjectFormView,
) )
from judge.utils.infinite_paginator import InfinitePaginationMixin
from .contests import ContestRanking from .contests import ContestRanking
__all__ = [ __all__ = [
@ -437,7 +437,7 @@ def edit_profile(request):
) )
class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView): class UserList(QueryStringSortMixin, InfinitePaginationMixin, TitleMixin, ListView):
model = Profile model = Profile
title = gettext_lazy("Leaderboard") title = gettext_lazy("Leaderboard")
context_object_name = "users" context_object_name = "users"
@ -449,12 +449,12 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
filter_friend = False filter_friend = False
def filter_friend_queryset(self, queryset): def filter_friend_queryset(self, queryset):
friends = list(self.request.profile.get_friends()) friends = self.request.profile.get_friends()
ret = queryset.filter(user__username__in=friends) ret = queryset.filter(id__in=friends)
return ret return ret
def get_queryset(self): def get_queryset(self):
ret = ( queryset = (
Profile.objects.filter(is_unlisted=False) Profile.objects.filter(is_unlisted=False)
.order_by(self.order, "id") .order_by(self.order, "id")
.select_related("user") .select_related("user")
@ -467,11 +467,13 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
"problem_count", "problem_count",
) )
) )
if self.request.organization:
queryset = queryset.filter(organizations=self.request.organization)
if (self.request.GET.get("friend") == "true") and self.request.profile: if (self.request.GET.get("friend") == "true") and self.request.profile:
ret = self.filter_friend_queryset(ret) queryset = self.filter_friend_queryset(queryset)
self.filter_friend = True self.filter_friend = True
return ret
return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs) context = super(UserList, self).get_context_data(**kwargs)

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
Django>=3.2,<4 Django>=3.2.17,<4
django_compressor>=3 django_compressor>=3
django-mptt>=0.13 django-mptt>=0.13
django-pagedown django-pagedown
@ -42,3 +42,4 @@ bleach
pymdown-extensions pymdown-extensions
mdx-breakless-lists mdx-breakless-lists
beautifulsoup4 beautifulsoup4
pre-commit

View file

@ -377,7 +377,7 @@ hr {
} }
#content { #content {
margin: 4.5em auto 1em auto; margin: 3.2em auto 1em auto;
// Header // Header
width: 90%; width: 90%;
@ -558,7 +558,6 @@ noscript #noscript {
margin: 0 auto; margin: 0 auto;
border-right: 1px solid $border_gray; border-right: 1px solid $border_gray;
border-left: 1px solid $border_gray; border-left: 1px solid $border_gray;
background: white;
} }
// border-bottom: 1px solid rgb(204, 204, 204) // border-bottom: 1px solid rgb(204, 204, 204)
@ -595,7 +594,7 @@ math {
} }
} }
@media (max-width: 760px) { @media (max-width: 799px) {
#navigation { #navigation {
height: 36px; height: 36px;
} }
@ -866,10 +865,15 @@ select {
.navbar-icons { .navbar-icons {
margin-top: 6px; margin-top: 6px;
} }
#page-container {
background: #f1f2f2;
} }
@media (min-width: 800px) {
#event-tab { #event-tab {
display: none; display: none;
} }
#content.wrapper {
background: white;
padding: 2em;
border-radius: 1em;
}
} }

View file

@ -32,7 +32,8 @@
} }
} }
.blog-sidebar, .right-sidebar { .blog-sidebar,
.right-sidebar {
width: 100%; width: 100%;
margin-left: auto; margin-left: auto;
} }
@ -41,6 +42,7 @@
color: green; color: green;
font-weight: bold; font-weight: bold;
background-color: lightgreen; background-color: lightgreen;
.sidebar-icon { .sidebar-icon {
color: green; color: green;
} }
@ -143,6 +145,7 @@
background-color: white; background-color: white;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
.title { .title {
margin-bottom: 0.2em; margin-bottom: 0.2em;
@ -150,24 +153,33 @@
} }
} }
.blog-box:hover {
border-color: #8a8a8a;
}
.blog-description { .blog-description {
max-height: 30em; max-height: 30em;
overflow: hidden; overflow: hidden;
overflow-wrap: anywhere; overflow-wrap: anywhere;
padding-bottom: 1em; padding-bottom: 1em;
clear: both; clear: both;
position: relative;
} }
.problem-feed-name { .problem-feed-name {
display: inline; display: inline;
font-weight: bold; font-weight: bold;
} }
.problem-feed-name a { .problem-feed-name a {
color: #0645ad; color: #0645ad;
} }
.problem-feed-info-entry { .problem-feed-info-entry {
display: inline; display: inline;
float: right; float: right;
} }
.problem-feed-types { .problem-feed-types {
color: gray; color: gray;
} }
@ -175,24 +187,30 @@
.left-sidebar-item { .left-sidebar-item {
display: flex; display: flex;
align-items: center; align-items: center;
.sidebar-icon { .sidebar-icon {
font-size: large; font-size: large;
display: inline-block; display: inline-block;
i { i {
width: 1.4em; width: 1.4em;
} }
} }
} }
.left-sidebar-item:hover { .left-sidebar-item:hover {
background-color: #e3e3e3; background-color: #e3e3e3;
cursor: pointer; cursor: pointer;
} }
.left-sidebar-item.active:hover { .left-sidebar-item.active:hover {
background-color: lightgreen; background-color: lightgreen;
} }
.sidebar-icon { .sidebar-icon {
color: black; color: black;
} }
.left-sidebar-header { .left-sidebar-header {
text-align: center; text-align: center;
padding-bottom: 1em; padding-bottom: 1em;
@ -200,15 +218,36 @@
color: black; color: black;
border-radius: 0; border-radius: 0;
} }
.feed-table { .feed-table {
margin: 0; margin: 0;
} }
.pre-expand-blog { .pre-expand-blog {
background-image: -webkit-linear-gradient(bottom, gray, lightgray 3%, transparent 8%, transparent 100%); position: relative;
padding-bottom: 0; padding-bottom: 0;
} }
.pre-expand-blog:hover {
background-color: #f3f3f3; .show-more {
display: flex;
color: black;
font-style: italic;
font-size: 16px;
font-weight: 700;
padding: 0px 12px;
margin-top: 5px;
position: absolute;
inset: 50% 0px 0px;
background: linear-gradient(transparent, white);
-webkit-box-pack: end;
justify-content: flex-end;
align-items: flex-end;
cursor: pointer;
padding: 16px 16px;
}
.actionbar-box {
margin: 8px 16px;
} }
.post-full { .post-full {
@ -219,11 +258,22 @@
} }
} }
.middle-right-content.wrapper {
padding: 1em 0;
background: white;
border-radius: 1em;
}
@media (max-width: 799px) { @media (max-width: 799px) {
.actionbar-box {
margin: 8px 0;
}
.left-sidebar-header { .left-sidebar-header {
display: none; display: none;
} }
.left-sidebar-item { .left-sidebar-item {
padding: 0.8em 0.2em 0.8em 0.2em; padding: 0.8em 0.2em 0.8em 0.2em;
display: inline-block; display: inline-block;
@ -234,25 +284,33 @@
display: none; display: none;
} }
} }
.left-sidebar { .left-sidebar {
text-align: center; text-align: center;
margin-bottom: 1em; margin-bottom: 1em;
border-radius: 7px; border-radius: 7px;
display: flex; display: flex;
} }
.blog-box { .blog-box {
padding-left: 5%; padding-left: 5%;
padding-right: 5%; padding-right: 5%;
margin-bottom: 0;
} }
.post-title { .post-title {
font-size: 2em; font-size: 2em;
} }
} }
@media (min-width: 800px) { @media (min-width: 800px) {
.left-sidebar-item { .left-sidebar-item {
padding: 0.8em 0.2em 0.8em 1em; padding: 0.8em 0.2em 0.8em 1em;
} }
.middle-content, .blog-sidebar, .right-sidebar {
.middle-content,
.blog-sidebar,
.right-sidebar {
display: block !important; display: block !important;
} }
@ -274,20 +332,22 @@
width: -webkit-fill-available; width: -webkit-fill-available;
} }
.blog-sidebar, .right-sidebar { .blog-sidebar,
.right-sidebar {
width: 25%; width: 25%;
} }
.left-sidebar { .left-sidebar {
width: 11%; width: 11%;
max-width: 11%;
min-width: 11%;
position: fixed; position: fixed;
height: 100%; height: 100%;
margin-top: -4em; margin-top: -4em;
padding-top: 4em; padding-top: 4em;
} border-right: 1px solid lightgray;
box-shadow: 0px -10px 2px 0px rgb(0 0 0 / 50%);
.left-sidebar-item { background: white;
border-radius: 0 0.5em 0.5em 0;
} }
.feed-table { .feed-table {
@ -297,7 +357,7 @@
.blog-box { .blog-box {
border-left: 1.4px solid lightgray; border-left: 1.4px solid lightgray;
border-right: 1.4px solid lightgray; border-right: 1.4px solid lightgray;
border-radius: 8px; border-radius: 16px;
} }
.post-full { .post-full {

View file

@ -1,3 +1,6 @@
.chat {
background: white;
}
#chat-log p { #chat-log p {
margin: 0; margin: 0;
padding-top: 0.1em; padding-top: 0.1em;

View file

@ -278,3 +278,9 @@ a {
color: rgb(180, 180, 7); color: rgb(180, 180, 7);
} }
} }
@media (max-width: 799px) {
.hide_texts_on_mobile .actionbar-text {
display: none;
}
}

View file

@ -359,6 +359,32 @@ function onWindowReady() {
$('#form-lang').submit(); $('#form-lang').submit();
}) })
$('#logout').on('click', () => $('#logout-form').submit()); $('#logout').on('click', () => $('#logout-form').submit());
var copyButton;
$('pre code').each(function () {
$(this).parent().before($('<div>', {'class': 'copy-clipboard'})
.append(copyButton = $('<span>', {
'class': 'btn-clipboard',
'data-clipboard-text': $(this).text(),
'title': 'Click to copy'
}).text('Copy')));
$(copyButton.get(0)).mouseleave(function () {
$(this).attr('class', 'btn-clipboard');
$(this).removeAttr('aria-label');
});
var curClipboard = new Clipboard(copyButton.get(0));
curClipboard.on('success', function (e) {
e.clearSelection();
showTooltip(e.trigger, 'Copied!');
});
curClipboard.on('error', function (e) {
showTooltip(e.trigger, fallbackMessage(e.action));
});
});
} }
$(function() { $(function() {

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,12 @@ $(function () {
csrfmiddlewaretoken: $.cookie('csrftoken') csrfmiddlewaretoken: $.cookie('csrftoken')
}, function (result) { }, function (result) {
$content.html(result); $content.html(result);
$(".dmmd-preview-content [data-src]img").each(function() {
$(this).attr("src", $(this).attr("data-src"));
})
$(".dmmd-preview-content [data-src]iframe").each(function() {
$(this).attr("src", $(this).attr("data-src"));
})
$preview.addClass('dmmd-preview-has-content').removeClass('dmmd-preview-stale'); $preview.addClass('dmmd-preview-has-content').removeClass('dmmd-preview-stale');
var $jax = $content.find('.require-mathjax-support'); var $jax = $content.find('.require-mathjax-support');

View file

@ -12,6 +12,6 @@ function mathjax_pagedown($) {
window.mathjax_pagedown = mathjax_pagedown; window.mathjax_pagedown = mathjax_pagedown;
$(window).load(function () { $(function () {
(mathjax_pagedown)('$' in window ? $ : django.jQuery); (mathjax_pagedown)('$' in window ? $ : django.jQuery);
}); });

View file

@ -369,7 +369,6 @@ ul.problem-list {
margin-left: 2.5%; margin-left: 2.5%;
padding-bottom: 1em; padding-bottom: 1em;
border-radius: 5px; border-radius: 5px;
margin-bottom: 1em;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }

View file

@ -5,7 +5,7 @@
} }
#submissions-table { #submissions-table {
background: rgba(0, 0, 0, .01); background: white;
} }
.submission-row { .submission-row {

View file

@ -12,6 +12,10 @@
background: #f7f7f7; background: #f7f7f7;
} }
&.striped tr:nth-child(odd) {
background: white;
}
td:first-child { td:first-child {
border-color: $border_gray; border-color: $border_gray;
border-width: 1px 1px 0 1px; border-width: 1px 1px 0 1px;

View file

@ -1,4 +1,5 @@
.ticket-container { .ticket-container {
display: flex;
#content > h2:first-child small { #content > h2:first-child small {
color: #999; color: #999;
font-size: 0.9em; font-size: 0.9em;

View file

@ -495,6 +495,7 @@ ul.select2-selection__rendered {
border-top: none; border-top: none;
margin: 0 -5px; margin: 0 -5px;
padding: 1px 0.5em 3px; padding: 1px 0.5em 3px;
background: white;
&.sidebox-table { &.sidebox-table {
border: none; border: none;
@ -510,7 +511,8 @@ ul.select2-selection__rendered {
border-top-left-radius: $widget_border_radius; border-top-left-radius: $widget_border_radius;
border-top-right-radius: $widget_border_radius; border-top-right-radius: $widget_border_radius;
padding: 0 5px !important; padding: 0 5px !important;
padding-bottom: 1.5em !important; margin-bottom: 1.5em !important;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
} }
.ws-closed { .ws-closed {

View file

@ -1,6 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
{% if request.organization %}
{% cache 3600 'organization_html' request.organization.id MATH_ENGINE %}
{{ request.organization.about|markdown|reference|str|safe }}
{% endcache %}
{% else %}
<h4> <h4>
<a target="_blank" href="">LQDOJ (Le Quy Don Online Judge)</a> là một trang web chấm bài tự động được phát triển dựa trên nền tảng mã nguồn mở <a target="_blank" href="https://dmoj.ca/">DMOJ</a>. Được xây dựng với mục đích ban đầu là tạo ra một môi trường học tập cho học sinh khối chuyên Tin <a target="_blank" href="http://thpt-lequydon-danang.edu.vn/">trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng)</a>, hiện nay LQDOJ đã cho phép đăng ký tự do để trở thành một sân chơi rộng mở cho toàn bộ cộng đồng học sinh yêu Tin học. Trang web cung cấp lượng bài luyện tập đồ sộ từ các kỳ thi HSG Quốc Gia, ACM ICPC, Olympic Duyên Hải Bắc Bộ, etc. cho đến các contest định kỳ để xếp loại khả năng (rating) giúp các bạn có thêm động lực cạnh tranh và khí thế phấn đấu rèn luyện nâng cao trình độ lập trình. Các bạn có thể tham khảo mã nguồn của trang web tại <a target="_blank" href="https://github.com/LQDJudge/online-judge">Github repo chính thức</a>. Mọi ý kiến đóng góp và thắc mắc xin gửi về: <a target="_blank" href="">LQDOJ (Le Quy Don Online Judge)</a> là một trang web chấm bài tự động được phát triển dựa trên nền tảng mã nguồn mở <a target="_blank" href="https://dmoj.ca/">DMOJ</a>. Được xây dựng với mục đích ban đầu là tạo ra một môi trường học tập cho học sinh khối chuyên Tin <a target="_blank" href="http://thpt-lequydon-danang.edu.vn/">trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng)</a>, hiện nay LQDOJ đã cho phép đăng ký tự do để trở thành một sân chơi rộng mở cho toàn bộ cộng đồng học sinh yêu Tin học. Trang web cung cấp lượng bài luyện tập đồ sộ từ các kỳ thi HSG Quốc Gia, ACM ICPC, Olympic Duyên Hải Bắc Bộ, etc. cho đến các contest định kỳ để xếp loại khả năng (rating) giúp các bạn có thêm động lực cạnh tranh và khí thế phấn đấu rèn luyện nâng cao trình độ lập trình. Các bạn có thể tham khảo mã nguồn của trang web tại <a target="_blank" href="https://github.com/LQDJudge/online-judge">Github repo chính thức</a>. Mọi ý kiến đóng góp và thắc mắc xin gửi về:
<ul> <ul>
@ -10,4 +15,5 @@
<li><a target="_blank" href="https://www.facebook.com/doannguyenthanhluong">Đoàn Nguyễn Thành Lương</a> (handle: <span class="rating rate-master user"><a target="_blank" href="../user/CaiWinDao">CaiWinDao</a></span>), email: <a href="mailto:luong@lqdoj.edu.vn">luong@lqdoj.edu.vn</a></li> <li><a target="_blank" href="https://www.facebook.com/doannguyenthanhluong">Đoàn Nguyễn Thành Lương</a> (handle: <span class="rating rate-master user"><a target="_blank" href="../user/CaiWinDao">CaiWinDao</a></span>), email: <a href="mailto:luong@lqdoj.edu.vn">luong@lqdoj.edu.vn</a></li>
</ul> </ul>
</h4> </h4>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -1,18 +1,22 @@
{% set logged_in = request.user.is_authenticated %} {% set logged_in = request.user.is_authenticated %}
{% set profile = request.profile if logged_in else None %} {% set profile = request.profile if logged_in else None %}
{% set hide_texts_on_mobile = (not hide_actionbar_comment) or actionbar_report_url %}
{% if logged_in %} {% if logged_in %}
{% if include_hr %}<hr>{% endif %} {% if include_hr %}
<div class="page-vote actionbar"> <hr>
{% endif %}
<div class="page-vote actionbar {{'hide_texts_on_mobile' if hide_texts_on_mobile}} ">
<span class="actionbar-block" style="justify-content: flex-start;"> <span class="actionbar-block" style="justify-content: flex-start;">
<span id="like-button-{{pagevote.id}}" <span id="like-button-{{pagevote.id}}"
class="like-button actionbar-button {% if pagevote.vote_score(request.profile) == 1 %}voted{% endif %}" class="like-button actionbar-button {% if pagevote.vote_score(request.profile) == 1 %}voted{% endif %}"
onclick="javascript:pagevote_upvote({{ pagevote.id }})" onclick="javascript:pagevote_upvote({{ pagevote.id }}, event)">
>
<span class="pagevote-score" id="pagevote-score-{{pagevote.id}}">{{ pagevote.score }}</span> <span class="pagevote-score" id="pagevote-score-{{pagevote.id}}">{{ pagevote.score }}</span>
<i class="fa fa-thumbs-o-up" style="font-size: large;"></i> <i class="fa fa-thumbs-o-up" style="font-size: large;"></i>
<span class="actionbar-text">{{_("Like")}}</span> <span class="actionbar-text">{{_("Like")}}</span>
</span> </span>
<span id="dislike-button-{{pagevote.id}}" class="dislike-button actionbar-button {% if pagevote.vote_score(request.profile) == -1 %}voted{% endif %}" onclick="javascript:pagevote_downvote({{ pagevote.id }})"> <span id="dislike-button-{{pagevote.id}}"
class="dislike-button actionbar-button {% if pagevote.vote_score(request.profile) == -1 %}voted{% endif %}"
onclick="javascript:pagevote_downvote({{ pagevote.id }}, event)">
<i class="fa fa-thumbs-o-down" style="font-size: large;"></i> <i class="fa fa-thumbs-o-down" style="font-size: large;"></i>
</span> </span>
</span> </span>
@ -20,21 +24,28 @@
<span class="actionbar-block"> <span class="actionbar-block">
<span class="actionbar-button actionbar-comment"> <span class="actionbar-button actionbar-comment">
<i class="fa fa-comment-o" style="font-size: large;"></i> <i class="fa fa-comment-o" style="font-size: large;"></i>
<span class="actionbar-text">{{_("Comment")}}</span> <span class="actionbar-text">
{{_("Comment")}}
</span>
{% if comment_list.count() %}
<span style="margin-left: 0.2em">
({{comment_list.count()}})
</span>
{% endif %}
</span> </span>
</span> </span>
{% endif %} {% endif %}
<span class="actionbar-block"> <span class="actionbar-block">
<span id="bookmark-button-{{bookmark.id}}" <span id="bookmark-button-{{bookmark.id}}"
class="bookmark-button actionbar-button {% if bookmark.get_bookmark(request.profile) == True %} bookmarked {% endif %}" class="bookmark-button actionbar-button {% if bookmark.get_bookmark(request.profile) == True %} bookmarked {% endif %}"
onclick="javascript:bookmark({{ bookmark.id }})" onclick="javascript:bookmark({{ bookmark.id }}, event)">
>
<i class="fa fa-bookmark-o" style="font-size: large;"></i> <i class="fa fa-bookmark-o" style="font-size: large;"></i>
<span class="actionbar-text">{{_("Bookmark")}}</span> <span class="actionbar-text">{{_("Bookmark")}}</span>
</span> </span>
</span> </span>
<span class="actionbar-block" > <span class="actionbar-block" style="justify-content: flex-end;">
<span class="actionbar-button actionbar-share" style="position: relative" {{"share-url=" + share_url if share_url else ""}}> <span class="actionbar-button actionbar-share" style="position: relative"
{{"share-url=" + share_url if share_url else ""}} onclick="javascript:actionbar_share(this, event)">
<i class=" fa fa-share" style="font-size: large;"></i> <i class=" fa fa-share" style="font-size: large;"></i>
<span class="actionbar-text">{{_("Share")}}</span> <span class="actionbar-text">{{_("Share")}}</span>
</span> </span>

View file

@ -1,7 +0,0 @@
<style>
@media (max-width: 799px) {
.actionbar-text {
display: none;
}
}
</style>

View file

@ -37,7 +37,8 @@
}); });
} }
window.bookmark = function(id) { window.bookmark = function(id, e) {
e.stopPropagation();
var $bookmark = $('#bookmark-button-' + id); var $bookmark = $('#bookmark-button-' + id);
if ($bookmark.hasClass('bookmarked')) { if ($bookmark.hasClass('bookmarked')) {
ajax_bookmark('{{ url('undobookmark') }}', id, function () { ajax_bookmark('{{ url('undobookmark') }}', id, function () {
@ -61,7 +62,8 @@
}; };
}; };
window.pagevote_upvote = function (id) { window.pagevote_upvote = function (id, e) {
e.stopPropagation();
var $votes = get_$votes(id); var $votes = get_$votes(id);
if ($votes.upvote.hasClass('voted')) { if ($votes.upvote.hasClass('voted')) {
ajax_vote('{{ url('pagevote_downvote') }}', id, -1, function () { ajax_vote('{{ url('pagevote_downvote') }}', id, -1, function () {
@ -83,7 +85,8 @@
} }
}; };
window.pagevote_downvote = function (id) { window.pagevote_downvote = function (id, e) {
e.stopPropagation();
var $votes = get_$votes(id); var $votes = get_$votes(id);
if ($votes.downvote.hasClass('voted')) { if ($votes.downvote.hasClass('voted')) {
ajax_vote('{{ url('pagevote_upvote') }}', id, 1, function () { ajax_vote('{{ url('pagevote_upvote') }}', id, 1, function () {
@ -104,19 +107,18 @@
} }
} }
}; };
$(".actionbar-share").click( function() { window.actionbar_share = function(element, e) {
link = $(this).attr("share-url") || window.location.href; e.stopPropagation();
link = $(element).attr("share-url") || window.location.href;
navigator.clipboard navigator.clipboard
.writeText(link) .writeText(link)
.then(() => { .then(() => {
showTooltip(this, "Copied link", 'n'); showTooltip(element, "Copied link", 'n');
});
}); });
};
$('.actionbar-comment').on('click', function() { $('.actionbar-comment').on('click', function() {
if ($('#comment-announcement').length) { $('#comment-section').show();
$('#comment-announcement').click();
}
$('#write-comment').click(); $('#write-comment').click();
}) })
}); });

View file

@ -89,6 +89,7 @@
<script src="{{ static('libs/jquery.unveil.js') }}"></script> <script src="{{ static('libs/jquery.unveil.js') }}"></script>
<script src="{{ static('libs/moment.js') }}"></script> <script src="{{ static('libs/moment.js') }}"></script>
<script src="{{ static('libs/select2/select2.js') }}"></script> <script src="{{ static('libs/select2/select2.js') }}"></script>
<script src="{{ static('libs/clipboard/clipboard.js') }}"></script>
{% include "extra_js.html" %} {% include "extra_js.html" %}
<script src="{{ static('common.js') }}"></script> <script src="{{ static('common.js') }}"></script>
<script src="{{ static('libs/clipboard/tooltip.js') }}"></script> <script src="{{ static('libs/clipboard/tooltip.js') }}"></script>
@ -331,7 +332,7 @@
<div id="noscript">{{ _('This site works best with JavaScript enabled.') }}</div> <div id="noscript">{{ _('This site works best with JavaScript enabled.') }}</div>
</noscript> </noscript>
<br> <br>
<main id="content"> <main id="content" class="{{'wrapper' if layout != 'no_wrapper'}}">
{% block title_row %} {% block title_row %}
<h2 class="title-row"> <h2 class="title-row">
{% block content_title %} {% block content_title %}

View file

@ -7,7 +7,6 @@
{% block media %} {% block media %}
{% include "comments/media-css.html" %} {% include "comments/media-css.html" %}
{% include "actionbar/media-css.html" %}
{% endblock %} {% endblock %}
{% block title_row %} {% block title_row %}
@ -30,7 +29,8 @@
</span> </span>
{% if post.is_editable_by(request.user) %} {% if post.is_editable_by(request.user) %}
<span> [<a href="{{ url('admin:judge_blogpost_change', post.id) }}">{{ _('Edit') }}</a>]</span> <span> [<a href="{{ url('admin:judge_blogpost_change', post.id) }}">{{ _('Edit') }}</a>]</span>
{% elif valid_user_to_show_edit %} {% endif %}
{% if valid_user_to_show_edit %}
{% for org in valid_org_to_show_edit %} {% for org in valid_org_to_show_edit %}
<span> [<a href="{{ url('edit_organization_blog', org.id , org.slug , post.id) }}">{{ _('Edit in') }} {{org.slug}}</a>]</span> <span> [<a href="{{ url('edit_organization_blog', org.id , org.slug , post.id) }}">{{ _('Edit in') }} {{org.slug}}</a>]</span>
{% endfor %} {% endfor %}

View file

@ -1,3 +1,4 @@
{% for post in posts%}
<section class="{% if post.sticky %}sticky {% endif %}blog-box"> <section class="{% if post.sticky %}sticky {% endif %}blog-box">
<div style="margin-bottom: 0.5em"> <div style="margin-bottom: 0.5em">
<span class="time"> <span class="time">
@ -28,7 +29,7 @@
<a href="{{ url('blog_post', post.id, post.slug) }}#comments" class="blog-comment-count-link"> <a href="{{ url('blog_post', post.id, post.slug) }}#comments" class="blog-comment-count-link">
<i class="fa fa-comments blog-comment-icon"></i> <i class="fa fa-comments blog-comment-icon"></i>
<span class="blog-comment-count"> <span class="blog-comment-count">
{{- post_comment_counts[post.id] or 0 -}} {{- post.comments.filter(hidden=False).count() or 0 -}}
</span> </span>
</a> </a>
</span> </span>
@ -42,11 +43,16 @@
{{ post.summary|default(post.content, true)|markdown(lazy_load=True)|reference|str|safe }} {{ post.summary|default(post.content, true)|markdown(lazy_load=True)|reference|str|safe }}
{% endcache %} {% endcache %}
</div> </div>
<div class="show-more"> {{_("...More")}} </div>
</div>
<div class="actionbar-box">
{% set pagevote = post.pagevote %} {% set pagevote = post.pagevote %}
{% set bookmark = post.bookmark %} {% set bookmark = post.bookmark %}
{% set hide_actionbar_comment = True %} {% set hide_actionbar_comment = True %}
{% set include_hr = True %} {% set include_hr = False %}
{% set share_url = request.build_absolute_uri(post.get_absolute_url()) %} {% set share_url = request.build_absolute_uri(post.get_absolute_url()) %}
{% include "actionbar/list.html" %} {% include "actionbar/list.html" %}
</div> </div>
</section> </section>
{% endfor %}
{% include "feed/has_next.html" %}

View file

@ -1,7 +1,6 @@
{% extends "three-column-content.html" %} {% extends "three-column-content.html" %}
{% block three_col_media %} {% block three_col_media %}
{% include "blog/media-css.html" %} {% include "blog/media-css.html" %}
{% include "actionbar/media-css.html" %}
<style> <style>
@media (max-width: 799px) { @media (max-width: 799px) {
.title { .title {
@ -21,6 +20,7 @@
{% block three_col_js %} {% block three_col_js %}
{% include "actionbar/media-js.html" %} {% include "actionbar/media-js.html" %}
{% include "feed/feed_js.html" %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$('.time-remaining').each(function () { $('.time-remaining').each(function () {
@ -51,24 +51,15 @@
{% block middle_content %} {% block middle_content %}
{% set show_organization_private_icon=True %} {% set show_organization_private_icon=True %}
{% if page_type == 'blog' %} {% if page_type == 'blog' %}
{% for post in posts %}
{% include "blog/content.html" %} {% include "blog/content.html" %}
{% endfor %}
{% elif page_type == 'ticket' %} {% elif page_type == 'ticket' %}
{% if tickets %} {% if tickets %}
{% for ticket in tickets %}
{% include "ticket/feed.html" %} {% include "ticket/feed.html" %}
{% endfor %}
{% else %} {% else %}
<h3 style="text-align: center">{{_('You have no ticket')}}</h3> <h3 style="text-align: center">{{_('You have no ticket')}}</h3>
{% endif %} {% endif %}
{% elif page_type == 'comment' %} {% elif page_type == 'comment' %}
{% for comment in comments %}
{% include "comments/feed.html" %} {% include "comments/feed.html" %}
{% endfor %}
{% endif %}
{% if page_obj.num_pages > 1 %}
<div style="margin-bottom:10px;margin-top:10px">{% include "list-pages.html" %}</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,3 +1,4 @@
{% set layout = "no_wrapper" %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title_row %}{% endblock %} {% block title_row %}{% endblock %}
{% block title_ruler %}{% endblock %} {% block title_ruler %}{% endblock %}
@ -22,25 +23,23 @@ let META_HEADER = [
]; ];
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
window.currentPage = 1;
window.limit_time = 24; window.limit_time = 24;
window.messages_per_page = 50;
window.room_id = "{{room if room else ''}}"; window.room_id = "{{room if room else ''}}";
window.unread_message = 0; window.unread_message = 0;
window.other_user_id = "{{other_user.id if other_user else ''}}"; window.other_user_id = "{{other_user.id if other_user else ''}}";
window.num_pages = {{paginator.num_pages}};
window.lock = false; window.lock = false;
window.lock_click_space = false; window.lock_click_space = false;
window.pushed_messages = new Set(); window.pushed_messages = new Set();
let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches; let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches;
function load_page(page, refresh_html=false) { function load_next_page(last_id, refresh_html=false) {
var param = { var param = {
'page': page, 'last_id': last_id,
'only_messages': true,
} }
$.get("{{ url('chat', '') }}" + window.room_id, param) $.get("{{ url('chat', '') }}" + window.room_id, param)
.fail(function() { .fail(function() {
console.log("Fail to load page " + page); console.log("Fail to load page, last_id = " + last_id);
}) })
.done(function(data) { .done(function(data) {
if (refresh_html) { if (refresh_html) {
@ -48,11 +47,10 @@ let META_HEADER = [
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight); $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
window.lock = true; window.lock = true;
} }
var time = refresh_html ? 0 : 200;
window.num_pages = parseInt($('<div>' + data + '</div>').find('#num_pages').html());
var time = refresh_html ? 0 : 500;
setTimeout(function() { setTimeout(function() {
$(".has_next").remove();
let $chat_box = $('#chat-box'); let $chat_box = $('#chat-box');
let lastMsgPos = scrollTopOfBottom($chat_box) let lastMsgPos = scrollTopOfBottom($chat_box)
@ -65,9 +63,6 @@ let META_HEADER = [
$('#chat-log').prepend(data); $('#chat-log').prepend(data);
} }
$('.body-block').slice(0, window.messages_per_page).each(function() {
});
register_time($('.time-with-rel')); register_time($('.time-with-rel'));
merge_authors(); merge_authors();
@ -78,6 +73,7 @@ let META_HEADER = [
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight); $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
} }
window.lock = false; window.lock = false;
window.has_next = parseInt($(".has_next").attr("value"));
}, time); }, time);
}) })
} }
@ -89,10 +85,12 @@ let META_HEADER = [
function scrollContainer(container, loader) { function scrollContainer(container, loader) {
container.scroll(function() { container.scroll(function() {
if (container.scrollTop() == 0) { if (container.scrollTop() == 0) {
if (currentPage < window.num_pages && !window.lock) { if (!window.lock && window.has_next) {
currentPage++;
loader.show(); loader.show();
load_page(currentPage); var message_ids = $('.message').map(function() {
return parseInt($(this).attr('message-id'));
}).get();
load_next_page(Math.min(...message_ids));
} }
} }
})} })}
@ -322,7 +320,7 @@ let META_HEADER = [
function callback() { function callback() {
history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id); history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id);
load_page(window.currentPage, true, refresh_status); load_next_page(null, true);
update_last_seen(); update_last_seen();
refresh_status(); refresh_status();
$('#chat-input').focus(); $('#chat-input').focus();
@ -331,7 +329,6 @@ let META_HEADER = [
if (encrypted_user) { if (encrypted_user) {
$.get("{{url('get_or_create_room')}}" + `?other=${encrypted_user}`) $.get("{{url('get_or_create_room')}}" + `?other=${encrypted_user}`)
.done(function(data) { .done(function(data) {
window.currentPage = 1;
window.room_id = data.room; window.room_id = data.room;
window.other_user_id = data.other_user_id; window.other_user_id = data.other_user_id;
callback(); callback();
@ -341,7 +338,6 @@ let META_HEADER = [
}) })
} }
else { else {
window.currentPage = 1;
window.room_id = ''; window.room_id = '';
window.other_user_id = ''; window.other_user_id = '';
callback(); callback();
@ -407,6 +403,7 @@ let META_HEADER = [
$(function() { $(function() {
$('#loader').hide(); $('#loader').hide();
merge_authors(); merge_authors();
window.has_next = parseInt($(".has_next").attr("value"));
scrollContainer($('#chat-box'), $('#loader')) scrollContainer($('#chat-box'), $('#loader'))

View file

@ -1,4 +1,4 @@
<li class="message" id="message-{{ message.id }}"> <li class="message" id="message-{{ message.id }}" message-id="{{ message.id }}">
<a href="{{ url('user_page', message.author.user.username) }}"> <a href="{{ url('user_page', message.author.user.username) }}">
<img src="{{ gravatar(message.author, 135) }}" class="profile-pic"> <img src="{{ gravatar(message.author, 135) }}" class="profile-pic">
</a> </a>
@ -21,7 +21,7 @@
</a> </a>
{% endif %} {% endif %}
<div class="message-text message-text-other"> <div class="message-text message-text-other">
{{message.body|markdown(lazy_load=True)|reference|str|safe }} {{message.body|markdown(lazy_load=False)|reference|str|safe }}
</div> </div>
</div> </div>
</span> </span>

View file

@ -1,3 +1,4 @@
<div class="has_next" style="display: none;" value="{{1 if has_next else 0}}"></div>
{% if object_list %} {% if object_list %}
<div style="display: none" id="num_pages">{{num_pages}}</div> <div style="display: none" id="num_pages">{{num_pages}}</div>
{% for message in object_list | reverse%} {% for message in object_list | reverse%}

View file

@ -1,7 +1,8 @@
{% for comment in comments %}
<div class="blog-box"> <div class="blog-box">
<h3 class="problem-feed-name"> <h3 class="problem-feed-name">
<a href="{{ comment.link }}#comment-{{ comment.id }}"> <a href="{{ comment.get_absolute_url() }}">
{{ page_titles[comment.page] }} {{ comment.page_title }}
</a> </a>
</h3> </h3>
{% with author=comment.author %} {% with author=comment.author %}
@ -14,5 +15,8 @@
{% endwith %} {% endwith %}
<div class='blog-description content-description'> <div class='blog-description content-description'>
{{ comment.body|markdown(lazy_load=True)|reference|str|safe }} {{ comment.body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div> </div>
</div> </div>
{% endfor %}
{% include "feed/has_next.html" %}

View file

@ -134,10 +134,7 @@
{% if node.score <= vote_hide_threshold %} {% if node.score <= vote_hide_threshold %}
<div class="comment-body bad-comment-body"> <div class="comment-body bad-comment-body">
<p> <p>
{% trans id=node.id %} {% trans id=node.id %}This comment is hidden due to too much negative feedback. Click <a href="javascript:comment_show_content({{ id }})">here</a> to view it.{% endtrans %}
This comment is hidden due to too much negative feedback.
Click <a href="javascript:comment_show_content({{ id }})">here</a> to view it.
{% endtrans %}
</p> </p>
</div> </div>
{% endif %} {% endif %}

View file

@ -1,3 +1,3 @@
{% with node=revision.field_dict %} {% with node=revision.object %}
<div class="comment-body">{{ node.body|markdown|reference|str|safe }}</div> <div class="comment-body">{{ node.body|markdown|reference|str|safe }}</div>
{% endwith %} {% endwith %}

View file

@ -6,7 +6,6 @@
{% block js_media %} {% block js_media %}
{% compress js %} {% compress js %}
<script src="{{ static('libs/clipboard/clipboard.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function () {
var info_float = $('.info-float'); var info_float = $('.info-float');
@ -23,6 +22,7 @@
} }
} }
// TODO: remove this
var copyButton; var copyButton;
$('pre code').each(function () { $('pre code').each(function () {
$(this).parent().before($('<div>', {'class': 'copy-clipboard'}) $(this).parent().before($('<div>', {'class': 'copy-clipboard'})

View file

@ -18,7 +18,6 @@
{% block two_col_media %} {% block two_col_media %}
{% include "comments/media-css.html" %} {% include "comments/media-css.html" %}
{% include "actionbar/media-css.html" %}
{% endblock %} {% endblock %}
{% block middle_content %} {% block middle_content %}
@ -31,7 +30,7 @@
{# Allow users to leave the virtual contest #} {# Allow users to leave the virtual contest #}
{% if in_contest %} {% if in_contest %}
<form action="{{ url('contest_leave', contest.key) }}" method="post" <form action="{{ url('contest_leave', contest.key) }}" method="post"
class="contest-join-pseudotab btn-midnightblue"> class="contest-join-pseudotab btn-red">
{% csrf_token %} {% csrf_token %}
<input type="submit" class="leaving-forever" value="{{ _('Leave contest') }}"> <input type="submit" class="leaving-forever" value="{{ _('Leave contest') }}">
</form> </form>
@ -83,6 +82,13 @@
{{ contest.description|markdown|reference|str|safe }} {{ contest.description|markdown|reference|str|safe }}
{% endcache %} {% endcache %}
</div> </div>
{% if editable_organizations %}
<div>
{% for org in editable_organizations %}
<span> [<a href="{{ url('organization_contest_edit', org.id , org.slug , contest.key) }}">{{ _('Edit in') }} {{org.slug}}</a>]</span>
{% endfor %}
</div>
{% endif %}
{% if contest.ended or request.user.is_superuser or is_editor or is_tester %} {% if contest.ended or request.user.is_superuser or is_editor or is_tester %}
<hr> <hr>

View file

@ -71,6 +71,10 @@
<script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script> <script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
var $form = $('form#filter-form');
$('input#show_orgs').click(function () {
$form.submit();
});
$('.time-remaining').each(function () { $('.time-remaining').each(function () {
count_down($(this)); count_down($(this));
}); });
@ -138,7 +142,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if contest.is_rated %} {% if contest.is_rated %}
<span class="contest-tag-rated"> <span class="contest-tag contest-tag-rated">
<i class="fa fa-bar-chart"></i> {{ _('rated') }} <i class="fa fa-bar-chart"></i> {{ _('rated') }}
</span> </span>
{% endif %} {% endif %}
@ -215,6 +219,12 @@
</select> </select>
{% endif %} {% endif %}
<button id="search-btn" class="btn-green small"> {{ _('Search')}} </button> <button id="search-btn" class="btn-green small"> {{ _('Search')}} </button>
{% if organizations %}
<div>
<input id="show_orgs" type="checkbox" name="show_orgs" value="1" {% if show_orgs %}checked{% endif %}>
<label for="show_orgs">{{ _('Hide organization contests') }}</label>
</div>
{% endif %}
{% if create_url %} {% if create_url %}
<a href="{{create_url}}" class="button small" style="float: right"><i class="fa fa-plus"></i> {{ _('Create')}}</a> <a href="{{create_url}}" class="button small" style="float: right"><i class="fa fa-plus"></i> {{ _('Create')}}</a>
{% endif %} {% endif %}

View file

@ -1,7 +1,5 @@
{% extends "user/base-users-table.html" %} {% extends "user/base-users-table.html" %}
{% set friends = request.profile.get_friends() if request.user.is_authenticated else {} %}
{% block after_rank_head %} {% block after_rank_head %}
{% if has_rating %} {% if has_rating %}
<th class="rating-column">{{ _('Rating') }}</th> <th class="rating-column">{{ _('Rating') }}</th>

View file

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

View file

@ -0,0 +1,4 @@
<h1> {{ title }} - Home </h1>
<h2> About: {{ description }} </h2>
<a href="/courses/{{ course.pk }}-{{ course.slug }}/resource"> See course resource.</a>

View file

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

View file

@ -0,0 +1,12 @@
<h1> {{ object }} </h1>
<h2> This resource belong to {{ object.course }}</h2>
<h3> {{ object.description }}</h3>
<div>
{% if object.files != None %}
<div> <a href="{{ object.files.url}}" target="_blank"> See files </a> </div>
{% endif %}
<div> <a href="/courses/{{ object.course.pk }}-{{ object.course.slug }}/resource/{{ object.pk }}/edit"> Edit
Resource </a> </div>
</div>

View file

@ -0,0 +1,11 @@
<h1> {{ course.name }}'s Resource </h1>
<ul>
{% for resource in object_list %}
<li>
<a href="/courses/{{ course.pk }}-{{ course.slug }}/resource/{{ resource.pk }}"> {{ resource }} </a>
</li>
{% endfor %}
</ul>
<a href="/courses/{{ course.pk }}-{{ course.slug }}/resource_edit"> Edit resources</a>

View file

@ -0,0 +1,5 @@
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<input type="submit" value="Update">
</form>

View file

@ -0,0 +1,33 @@
{{ course }}
<form id="resource-form" method="post" action="">
{% csrf_token %}
<table>
<tr>
<th>Name</th>
<th>Delete</th>
<th>Order</th>
<th>Public</th>
<th>Edit link</th>
</tr>
{% for resource in object_list %}
<tr>
<td> {{ resource }} </td>
<td> <input type="checkbox" name="resource-{{ resource.pk }}-delete" id="resource-{{ resource.pk }}-delete">
</td>
<td> <input type="number" name="resource-{{ resource.pk }}-order" value="{{
resource.order }}" id="resource-{{ resource.pk }}-order"> </a></td>
{% if resource.is_public %}
<td> <input type="checkbox" name="resource-{{ resource.pk }}-public" id="resource-{{ resource.pk }}-public"
checked=""> </a></td>
{% else %}
<td> <input type="checkbox" name="resource-{{ resource.pk }}-public" id="resource-{{ resource.pk }}-public">
</a></td>
{% endif %}
<td> <a href="/courses/{{ course.pk }}-{{ course.slug }}/resource/{{ resource.pk }}/edit"> Edit </a></td>
</tr>
{% endfor %}
</table>
<button type="submit">Save</button>
</form>

View file

@ -0,0 +1,30 @@
<script>
window.page = {{page_obj.number}};
window.has_next_page = {{1 if page_obj.has_next() else 0}};
window.loading_page = false;
$(function() {
$(window).on("scroll", function() {
if (window.loading_page || !window.has_next_page) return;
var distanceFromBottom = $(document).height() - ($(window).scrollTop() + $(window).height());
if (distanceFromBottom < 500) {
window.loading_page = true;
var params = {
"only_content": 1,
"page": window.page + 1,
};
$.get("{{feed_content_url}}", params)
.done(function(data) {
$(".has_next").remove();
$(".middle-content").append(data);
window.loading_page = false;
window.has_next_page = parseInt($(".has_next").attr("value"));
window.page++;
MathJax.typeset($('.middle-content')[0]);
onWindowReady();
activateBlogBoxOnClick();
})
}
});
});
</script>

View file

@ -0,0 +1 @@
<div class="has_next" style="display: none;" value="{{1 if has_next_page else 0}}"></div>

View file

@ -29,7 +29,7 @@
</td> </td>
<td> <td>
{% if notification.comment %} {% if notification.comment %}
<a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ page_titles[notification.comment.page] }}</a> <a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ notification.comment.page_title }}</a>
{% else %} {% else %}
{% autoescape off %} {% autoescape off %}
{{notification.html_link}} {{notification.html_link}}

View file

@ -12,7 +12,8 @@
#org-field-wrapper-start_time, #org-field-wrapper-start_time,
#org-field-wrapper-end_time, #org-field-wrapper-end_time,
#org-field-wrapper-time_limit, #org-field-wrapper-time_limit,
#org-field-wrapper-format_name { #org-field-wrapper-format_name,
#org-field-wrapper-freeze_after {
display: inline-flex; display: inline-flex;
} }
.problems-problem { .problems-problem {

View file

@ -4,16 +4,22 @@
{% block org_js %} {% block org_js %}
{% include "actionbar/media-js.html" %} {% include "actionbar/media-js.html" %}
{% endblock %} {% include "feed/feed_js.html" %}
{% block three_col_media %}
{% include "actionbar/media-css.html" %}
{% endblock %} {% endblock %}
{% block middle_title %} {% block middle_title %}
<div class="page-title"> <div class="page-title">
<div class="tabs" style="border: none;"> <div class="tabs" style="border: none;">
<h2><img src="{{logo_override_image}}" style="height: 3rem; vertical-align: middle"> <span>{{title}}</span></h2> <h2><img src="{{logo_override_image}}" style="height: 3rem; vertical-align: middle">
{{title}}
</h2>
{% if is_member %}
<div>
<a href="{{organization_subdomain}}" target="_blank">
{{_('Subdomain')}}
</a>
</div>
{% endif %}
<span class="spacer"></span> <span class="spacer"></span>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
@ -35,12 +41,7 @@
{% block middle_content %} {% block middle_content %}
{% block before_posts %}{% endblock %} {% block before_posts %}{% endblock %}
{% if is_member or can_edit %} {% if is_member or can_edit %}
{% for post in posts %}
{% include "blog/content.html" %} {% include "blog/content.html" %}
{% endfor %}
{% if posts.paginator.num_pages > 1 %}
<div style="margin-bottom:10px;margin-top:10px">{% include "list-pages.html" %}</div>
{% endif %}
{% else %} {% else %}
<div class="blog-sidebox sidebox"> <div class="blog-sidebox sidebox">
<h3>{{ _('About') }}<i class="fa fa-info-circle"></i></h3> <h3>{{ _('About') }}<i class="fa fa-info-circle"></i></h3>

View file

@ -56,7 +56,7 @@
</div> </div>
</li> </li>
{% endif %} {% endif %}
{% if is_member and not can_edit %} {% if is_member and not is_admin %}
<li> <li>
<form method="post" action="{{ url('leave_organization', organization.id, organization.slug) }}"> <form method="post" action="{{ url('leave_organization', organization.id, organization.slug) }}">
{% csrf_token %} {% csrf_token %}

View file

@ -7,7 +7,6 @@
{% block content_media %} {% block content_media %}
{% include "comments/media-css.html" %} {% include "comments/media-css.html" %}
{% include "actionbar/media-css.html" %}
{% endblock %} {% endblock %}
{% block header %} {% block header %}

View file

@ -2,17 +2,22 @@
{% block left_sidebar %} {% block left_sidebar %}
{% include "problem/left-sidebar.html" %} {% include "problem/left-sidebar.html" %}
{% endblock %} {% endblock %}
{% block problem_list_js %}
{% include "feed/feed_js.html" %}
{% endblock %}
{% block middle_content %} {% block middle_content %}
<div class="problem-feed-option"> <div class="problem-feed-option">
<a href="{{url('problem_feed')}}" class="button small {{'btn-midnightblue' if feed_type=='for_you' else 'btn-gray'}}"> <a href="{{url('problem_feed')}}"
class="button small {{'btn-midnightblue' if feed_type=='for_you' else 'btn-gray'}}">
{{_('FOR YOU')}} {{_('FOR YOU')}}
</a> </a>
<a href="{{url('problem_feed_new')}}" class="button small {{'btn-midnightblue' if feed_type=='new' else 'btn-gray'}}"> <a href="{{url('problem_feed_new')}}" class="button small {{'btn-midnightblue' if feed_type=='new' else 'btn-gray'}}">
{{_('NEW')}} {{_('NEW')}}
</a> </a>
{% if request.user.has_perm('judge.suggest_problem_changes') %} {% if request.user.has_perm('judge.suggest_problem_changes') %}
<a href="{{url('problem_feed_volunteer')}}" class="button small {{'btn-midnightblue' if feed_type=='volunteer' else 'btn-gray'}}"> <a href="{{url('problem_feed_volunteer')}}"
class="button small {{'btn-midnightblue' if feed_type=='volunteer' else 'btn-gray'}}">
{{_('VOLUNTEER')}} {{_('VOLUNTEER')}}
</a> </a>
{% endif %} {% endif %}
@ -22,120 +27,5 @@
<li><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('View your votes')}}</a></li> <li><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('View your votes')}}</a></li>
</ul> </ul>
{% endif %} {% endif %}
{% for problem in problems %} {% include "problem/feed/problems.html" %}
<div class="blog-box">
<h3 class="problem-feed-name">
<a href="{{ url('problem_detail', problem.code) }}">
{{ problem.i18n_name }}
</a>
{% if problem.id in completed_problem_ids %}
<i class="solved-problem-color fa fa-check-circle"></i>
{% elif problem.id in attempted_problems %}
<i class="attempted-problem-color fa fa-minus-circle"></i>
{% else %}
<i class="unsolved-problem-color fa fa-minus-circle"></i>
{% endif %}
</h3>
{% with authors=problem.authors.all() %}
{% if authors %}
<div class="problem-feed-info-entry">
<i class="fa fa-pencil-square-o fa-fw"></i>
<span class="pi-value">{{ link_users(authors) }}</span>
</div>
{% endif %}
{% endwith %}
{% if show_types %}
<div class="problem-feed-types">
<i class="fa fa-tag"></i>
{% for type in problem.types_list %}
<span class="type-tag">{{ type }}</span>{% if not loop.last %}, {% endif %}
{% endfor %}, *{{problem.points | int}}
</div>
{% endif %}
<div class="blog-description">
<div class='content-description'>
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
{{ problem.description|markdown(lazy_load=True)|reference|str|safe }}
{% endcache %}
{% if problem.pdf_description %}
<embed src="{{url('problem_pdf_description', problem.code)}}" width="100%" height="500" type="application/pdf" style="margin-top: 0.5em">
{% endif %}
</div>
{% set include_hr = True %}
{% set hide_actionbar_comment = True %}
{% set pagevote = problem.pagevote %}
{% set bookmark = problem.bookmark %}
{% set share_url = request.build_absolute_uri(problem.get_absolute_url()) %}
{% include "actionbar/list.html" %}
{% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %}
<br>
<a href="#" class="view-statement-src">{{ _('View source') }}</a>
<pre class="statement-src" style="display: none">{{ problem.description|str }}</pre>
<hr>
<center><h3>{{_('Volunteer form')}}</h3></center>
<br>
<button class="edit-btn" id="edit-{{problem.id}}" pid="{{problem.id}}" style="float: right">{{_('Edit')}}</button>
<form class="volunteer-form" id="form-{{problem.id}}" pid="{{problem.id}}" style="display: none;" method="POST">
<input type="submit" class="volunteer-submit-btn" id="submit-{{problem.id}}" pid="{{problem.id}}" pcode="{{problem.code}}" style="float: right" value="{{_('Submit')}}">
<table class="table">
<thead>
<tr>
<th>
</th>
<th>
{{_('Value')}}
</th>
</tr>
</thead>
<tbody>
<tr>
<td width="30%">
<label for="knowledge_point-{{problem.id}}"><i>{{ _('Knowledge point') }}</i></label>
</td>
<td>
<input id="knowledge_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="thinking_point-{{problem.id}}"><i>{{ _('Thinking point') }}</i></label>
</td>
<td>
<input id="thinking_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="types"><i>{{ _('Problem types') }}</i></label>
</td>
<td>
<select id="volunteer-types-{{problem.id}}" name="types" multiple>
{% for type in problem_types %}
<option value="{{ type.id }}"{% if type in problem.types.all() %} selected{% endif %}>
{{ type.full_name }}
</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td width="30%">
<label for="feedback"><i>{{ _('Feedback') }}</i></label>
</td>
<td>
<textarea id="feedback-{{problem.id}}" rows="2" style="width: 100%" placeholder="{{_('Any additional note here')}}"></textarea>
</td>
</tr>
</tbody>
</table>
</form>
<center id="thank-{{problem.id}}" style="display: none; margin-top: 3em"></center>
{% endif %}
</div>
</div>
{% endfor %}
{% if page_obj.num_pages > 1 %}
<div style="margin-top:10px;">{% include "list-pages.html" %}</div>
{% endif %}
{% endblock %} {% endblock %}

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