Merge branch 'LQDJudge:master' into master

This commit is contained in:
pcthuoc 2024-06-22 16:38:36 +07:00 committed by GitHub
commit ff9b86ea13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2370 changed files with 30872 additions and 13914 deletions

View file

@ -49,7 +49,7 @@
<br> <br>
<div class="popup"> <div class="popup">
<div> <div>
<img class="logo" src="logo.png" alt="LQDOJ"> <img class="logo" src="logo.svg" alt="LQDOJ">
</div> </div>
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1> <h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
</div> </div>

View file

@ -3,3 +3,6 @@ from django.apps import AppConfig
class ChatBoxConfig(AppConfig): class ChatBoxConfig(AppConfig):
name = "chat_box" name = "chat_box"
def ready(self):
from . import models

View file

@ -25,26 +25,18 @@ class Room(models.Model):
class Meta: class Meta:
app_label = "chat_box" app_label = "chat_box"
@cache_wrapper(prefix="Rinfo")
def _info(self):
last_msg = self.message_set.first()
return {
"user_ids": [self.user_one.id, self.user_two.id],
"last_message": last_msg.body if last_msg else None,
}
@cached_property @cached_property
def _cached_info(self): def _cached_info(self):
return self._info() return get_room_info(self.id)
def contain(self, profile): def contain(self, profile):
return profile.id in self._cached_info["user_ids"] return profile.id in [self.user_one_id, self.user_two_id]
def other_user(self, profile): def other_user(self, profile):
return self.user_one if profile == self.user_two else self.user_two return self.user_one if profile == self.user_two else self.user_two
def other_user_id(self, profile): def other_user_id(self, profile):
user_ids = self._cached_info["user_ids"] user_ids = [self.user_one_id, self.user_two_id]
return sum(user_ids) - profile.id return sum(user_ids) - profile.id
def users(self): def users(self):
@ -53,6 +45,10 @@ class Room(models.Model):
def last_message_body(self): def last_message_body(self):
return self._cached_info["last_message"] return self._cached_info["last_message"]
@classmethod
def prefetch_room_cache(self, room_ids):
get_room_info.prefetch_multi([(i,) for i in room_ids])
class Message(models.Model): class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
@ -66,7 +62,6 @@ class Message(models.Model):
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
new_message = self.id
self.body = self.body.strip() self.body = self.body.strip()
super(Message, self).save(*args, **kwargs) super(Message, self).save(*args, **kwargs)
@ -148,3 +143,11 @@ class Ignore(models.Model):
self.remove_ignore(current_user, friend) self.remove_ignore(current_user, friend)
else: else:
self.add_ignore(current_user, friend) self.add_ignore(current_user, friend)
@cache_wrapper(prefix="Rinfo")
def get_room_info(room_id):
last_msg = Message.objects.filter(room_id=room_id).first()
return {
"last_message": last_msg.body if last_msg else None,
}

View file

@ -34,7 +34,7 @@ from judge import event_poster as event
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
from judge.models import Friend from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
@ -174,19 +174,46 @@ def mute_message(request):
return JsonResponse(ret) return JsonResponse(ret)
def check_valid_message(request, room):
if not room and len(request.POST["body"]) > 200:
return False
if not can_access_room(request, room) or request.profile.mute:
return False
last_msg = Message.objects.filter(room=room).first()
if (
last_msg
and last_msg.author == request.profile
and last_msg.body == request.POST["body"].strip()
):
return False
if not room:
four_last_msg = Message.objects.filter(room=room).order_by("-id")[:4]
if len(four_last_msg) >= 4:
same_author = all(msg.author == request.profile for msg in four_last_msg)
time_diff = timezone.now() - four_last_msg[3].time
if same_author and time_diff.total_seconds() < 300:
return False
return True
@login_required @login_required
def post_message(request): def post_message(request):
ret = {"msg": "posted"} ret = {"msg": "posted"}
if request.method != "POST": if request.method != "POST":
return HttpResponseBadRequest() return HttpResponseBadRequest()
if len(request.POST["body"]) > 5000: if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
return HttpResponseBadRequest() return HttpResponseBadRequest()
room = None room = None
if request.POST["room"]: if request.POST["room"]:
room = Room.objects.get(id=request.POST["room"]) room = Room.objects.get(id=request.POST["room"])
if not can_access_room(request, room) or request.profile.mute: if not check_valid_message(request, room):
return HttpResponseBadRequest() return HttpResponseBadRequest()
new_message = Message(author=request.profile, body=request.POST["body"], room=room) new_message = Message(author=request.profile, body=request.POST["body"], room=room)
@ -204,7 +231,7 @@ def post_message(request):
}, },
) )
else: else:
Room._info.dirty(room) get_room_info.dirty(room.id)
room.last_msg_time = new_message.time room.last_msg_time = new_message.time
room.save() room.save()
@ -229,9 +256,7 @@ def post_message(request):
def can_access_room(request, room): def can_access_room(request, room):
return ( return not room or room.contain(request.profile)
not room or room.user_one == request.profile or room.user_two == request.profile
)
@login_required @login_required
@ -247,7 +272,7 @@ def chat_message_ajax(request):
try: try:
message = Message.objects.filter(hidden=False).get(id=message_id) message = Message.objects.filter(hidden=False).get(id=message_id)
room = message.room room = message.room
if room and not room.contain(request.profile): if not can_access_room(request, room):
return HttpResponse("Unauthorized", status=401) return HttpResponse("Unauthorized", status=401)
except Message.DoesNotExist: except Message.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -278,7 +303,7 @@ def update_last_seen(request, **kwargs):
except Room.DoesNotExist: except Room.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if room and not room.contain(profile): if not can_access_room(request, room):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room) user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
@ -338,6 +363,8 @@ def user_online_status_ajax(request):
def get_online_status(profile, other_profile_ids, rooms=None): def get_online_status(profile, other_profile_ids, rooms=None):
if not other_profile_ids: if not other_profile_ids:
return None return None
Profile.prefetch_profile_cache(other_profile_ids)
joined_ids = ",".join([str(id) for id in other_profile_ids]) joined_ids = ",".join([str(id) for id in other_profile_ids])
other_profiles = Profile.objects.raw( other_profiles = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})" f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
@ -404,6 +431,7 @@ def get_status_context(profile, include_ignored=False):
recent_profile_ids = [str(i["other_user"]) for i in recent_profile] recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
recent_rooms = [int(i["id"]) for i in recent_profile] recent_rooms = [int(i["id"]) for i in recent_profile]
Room.prefetch_room_cache(recent_rooms)
admin_list = ( admin_list = (
queryset.filter(display_rank="admin") queryset.filter(display_rank="admin")
@ -473,9 +501,16 @@ def get_or_create_room(request):
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.save() user_room.save()
room_url = reverse("chat", kwargs={"room_id": room.id})
if request.method == "GET": if request.method == "GET":
return JsonResponse({"room": room.id, "other_user_id": other_user.id}) return JsonResponse(
return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id})) {
"room": room.id,
"other_user_id": other_user.id,
"url": room_url,
}
)
return HttpResponseRedirect(room_url)
def get_unread_count(rooms, user): def get_unread_count(rooms, user):

View file

@ -34,6 +34,7 @@ SITE_ID = 1
SITE_NAME = "LQDOJ" SITE_NAME = "LQDOJ"
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge" SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
SITE_ADMIN_EMAIL = False SITE_ADMIN_EMAIL = False
SITE_DOMAIN = "lqdoj.edu.vn"
DMOJ_REQUIRE_STAFF_2FA = True DMOJ_REQUIRE_STAFF_2FA = True
@ -85,6 +86,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"ERR": "#ffa71c", "ERR": "#ffa71c",
} }
DMOJ_PROFILE_IMAGE_ROOT = "profile_images" DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {} MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {} MARKDOWN_DEFAULT_STYLE = {}
@ -130,13 +132,10 @@ USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = "chromedriver" SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
PYGMENT_THEME = "pygment-github.css"
INLINE_JQUERY = True INLINE_JQUERY = True
INLINE_FONTAWESOME = True INLINE_FONTAWESOME = True
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
FONTAWESOME_CSS = ( FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
)
DMOJ_CANONICAL = "" DMOJ_CANONICAL = ""
# Application definition # Application definition
@ -170,7 +169,7 @@ else:
}, },
{ {
"model": "judge.Submission", "model": "judge.Submission",
"icon": "fa-check-square-o", "icon": "fa-check-square",
"children": [ "children": [
"judge.Language", "judge.Language",
"judge.Judge", "judge.Judge",
@ -278,8 +277,11 @@ LANGUAGE_COOKIE_AGE = 8640000
FORM_RENDERER = "django.forms.renderers.TemplatesSetting" FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE_REQUIRE_SUPERUSER = True IMPERSONATE = {
IMPERSONATE_DISABLE_LOGGING = True "REQUIRE_SUPERUSER": True,
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
@ -323,7 +325,6 @@ TEMPLATES = [
"judge.template_context.site", "judge.template_context.site",
"judge.template_context.site_name", "judge.template_context.site_name",
"judge.template_context.misc_config", "judge.template_context.misc_config",
"judge.template_context.math_setting",
"social_django.context_processors.backends", "social_django.context_processors.backends",
"social_django.context_processors.login_redirect", "social_django.context_processors.login_redirect",
], ],
@ -431,7 +432,7 @@ AUTHENTICATION_BACKENDS = (
"social_core.backends.google.GoogleOAuth2", "social_core.backends.google.GoogleOAuth2",
"social_core.backends.facebook.FacebookOAuth2", "social_core.backends.facebook.FacebookOAuth2",
"judge.social_auth.GitHubSecureEmailOAuth2", "judge.social_auth.GitHubSecureEmailOAuth2",
"django.contrib.auth.backends.ModelBackend", "judge.authentication.CustomModelBackend",
) )
SOCIAL_AUTH_PIPELINE = ( SOCIAL_AUTH_PIPELINE = (
@ -488,6 +489,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload # Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp" CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
# Rate limit
RL_VOTE = "200/h"
RL_COMMENT = "30/h"
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

@ -16,14 +16,6 @@ from django.contrib.auth.decorators import login_required
from django.conf.urls.static import static as url_static from django.conf.urls.static import static as url_static
from judge.feed import (
AtomBlogFeed,
AtomCommentFeed,
AtomProblemFeed,
BlogFeed,
CommentFeed,
ProblemFeed,
)
from judge.forms import CustomAuthenticationForm from judge.forms import CustomAuthenticationForm
from judge.sitemap import ( from judge.sitemap import (
BlogPostSitemap, BlogPostSitemap,
@ -46,6 +38,7 @@ from judge.views import (
license, license,
mailgun, mailgun,
markdown_editor, markdown_editor,
test_formatter,
notification, notification,
organization, organization,
preview, preview,
@ -68,7 +61,12 @@ from judge.views import (
resolver, resolver,
course, course,
email, email,
custom_file_upload,
) )
from judge import authentication
from judge.views.test_formatter import test_formatter
from judge.views.problem_data import ( from judge.views.problem_data import (
ProblemDataView, ProblemDataView,
ProblemSubmissionDiff, ProblemSubmissionDiff,
@ -80,7 +78,6 @@ from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import ( from judge.views.select2 import (
AssigneeSelect2View, AssigneeSelect2View,
ChatUserSearchSelect2View, ChatUserSearchSelect2View,
CommentSelect2View,
ContestSelect2View, ContestSelect2View,
ContestUserSearchSelect2View, ContestUserSearchSelect2View,
OrganizationSelect2View, OrganizationSelect2View,
@ -88,6 +85,7 @@ from judge.views.select2 import (
TicketUserSelect2View, TicketUserSelect2View,
UserSearchSelect2View, UserSearchSelect2View,
UserSelect2View, UserSelect2View,
ProblemAuthorSearchSelect2View,
) )
admin.autodiscover() admin.autodiscover()
@ -144,9 +142,7 @@ register_patterns = [
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"), url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
url( url(
r"^password/change/$", r"^password/change/$",
auth_views.PasswordChangeView.as_view( authentication.CustomPasswordChangeView.as_view(),
template_name="registration/password_change_form.html",
),
name="password_change", name="password_change",
), ),
url( url(
@ -403,7 +399,28 @@ urlpatterns = [
name="submission_status", name="submission_status",
), ),
url(r"^/abort$", submission.abort_submission, name="submission_abort"), url(r"^/abort$", submission.abort_submission, name="submission_abort"),
url(r"^/html$", submission.single_submission), ]
),
),
url(
r"^test_formatter/",
include(
[
url(
r"^$",
login_required(test_formatter.TestFormatter.as_view()),
name="test_formatter",
),
url(
r"^edit_page$",
login_required(test_formatter.EditTestFormatter.as_view()),
name="test_formatter_edit",
),
url(
r"^download_page$",
login_required(test_formatter.DownloadTestFormatter.as_view()),
name="test_formatter_download",
),
] ]
), ),
), ),
@ -471,6 +488,7 @@ urlpatterns = [
reverse("all_user_submissions", args=[user]) reverse("all_user_submissions", args=[user])
), ),
), ),
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
url( url(
r"^/$", r"^/$",
lambda _, user: HttpResponsePermanentRedirect( lambda _, user: HttpResponsePermanentRedirect(
@ -519,11 +537,37 @@ urlpatterns = [
), ),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
url( url(
r"^contests/summary/(?P<key>\w+)$", r"^contests/summary/(?P<key>\w+)/",
contests.contests_summary_view, paged_list_view(contests.ContestsSummaryView, "contests_summary"),
name="contests_summary", ),
url(
r"^contests/official",
paged_list_view(contests.OfficialContestList, "official_contest_list"),
),
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
url(
r"^course/(?P<slug>[\w-]*)",
include(
[
url(r"^$", course.CourseDetail.as_view(), name="course_detail"),
url(
r"^/lesson/(?P<id>\d+)$",
course.CourseLessonDetail.as_view(),
name="course_lesson_detail",
),
url(
r"^/edit_lessons$",
course.EditCourseLessonsView.as_view(),
name="edit_course_lessons",
),
url(
r"^/grades$",
course.CourseStudentResults.as_view(),
name="course_grades",
),
]
),
), ),
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
url( url(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$", r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
contests.ContestCalendar.as_view(), contests.ContestCalendar.as_view(),
@ -587,6 +631,13 @@ urlpatterns = [
"contest_user_submissions_ajax", "contest_user_submissions_ajax",
), ),
), ),
url(
r"^/submissions",
paged_list_view(
submission.ContestSubmissions,
"contest_submissions",
),
),
url( url(
r"^/participations$", r"^/participations$",
contests.ContestParticipationList.as_view(), contests.ContestParticipationList.as_view(),
@ -852,6 +903,11 @@ urlpatterns = [
AssigneeSelect2View.as_view(), AssigneeSelect2View.as_view(),
name="ticket_assignee_select2_ajax", name="ticket_assignee_select2_ajax",
), ),
url(
r"^problem_authors$",
ProblemAuthorSearchSelect2View.as_view(),
name="problem_authors_select2_ajax",
),
] ]
), ),
), ),
@ -910,19 +966,6 @@ urlpatterns = [
] ]
), ),
), ),
url(
r"^feed/",
include(
[
url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"),
url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"),
url(r"^comment/rss/$", CommentFeed(), name="comment_rss"),
url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"),
url(r"^blog/rss/$", BlogFeed(), name="blog_rss"),
url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"),
]
),
),
url( url(
r"^stats/", r"^stats/",
include( include(
@ -1023,9 +1066,6 @@ urlpatterns = [
url( url(
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2" r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
), ),
url(
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
),
] ]
), ),
), ),
@ -1131,8 +1171,7 @@ urlpatterns = [
), ),
url( url(
r"^notifications/", r"^notifications/",
login_required(notification.NotificationList.as_view()), paged_list_view(notification.NotificationList, "notification"),
name="notification",
), ),
url( url(
r"^import_users/", r"^import_users/",
@ -1162,6 +1201,7 @@ urlpatterns = [
), ),
), ),
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"), url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"),
url(r"^upload/$", custom_file_upload.file_upload, name="custom_file_upload"),
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# if hasattr(settings, "INTERNAL_IPS"): # if hasattr(settings, "INTERNAL_IPS"):

View file

@ -20,9 +20,15 @@ from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin, UserAdmin from judge.admin.profile import ProfileAdmin, UserAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.taxon import (
ProblemGroupAdmin,
ProblemTypeAdmin,
OfficialContestCategoryAdmin,
OfficialContestLocationAdmin,
)
from judge.admin.ticket import TicketAdmin from judge.admin.ticket import TicketAdmin
from judge.admin.volunteer import VolunteerProblemVoteAdmin from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.admin.course import CourseAdmin
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
Comment, Comment,
@ -47,6 +53,8 @@ from judge.models import (
VolunteerProblemVote, VolunteerProblemVote,
Course, Course,
ContestsSummary, ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
) )
@ -72,7 +80,9 @@ 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(Course, CourseAdmin)
admin.site.unregister(User) admin.site.unregister(User)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(ContestsSummary, ContestsSummaryAdmin) admin.site.register(ContestsSummary, ContestsSummaryAdmin)
admin.site.register(OfficialContestCategory, OfficialContestCategoryAdmin)
admin.site.register(OfficialContestLocation, OfficialContestLocationAdmin)

View file

@ -12,7 +12,6 @@ class CommentForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
"author": AdminHeavySelect2Widget(data_view="profile_select2"), "author": AdminHeavySelect2Widget(data_view="profile_select2"),
"parent": AdminHeavySelect2Widget(data_view="comment_select2"),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets["body"] = HeavyPreviewAdminPageDownWidget( widgets["body"] = HeavyPreviewAdminPageDownWidget(
@ -39,7 +38,7 @@ class CommentAdmin(VersionAdmin):
) )
list_display = ["author", "linked_object", "time"] list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"] search_fields = ["author__user__username", "body"]
readonly_fields = ["score"] readonly_fields = ["score", "parent"]
actions = ["hide_comment", "unhide_comment"] actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"] list_filter = ["hidden"]
actions_on_top = True actions_on_top = True

View file

@ -14,7 +14,14 @@ from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin from reversion_compare.admin import CompareVersionAdmin
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating from judge.models import (
Contest,
ContestProblem,
ContestSubmission,
Profile,
Rating,
OfficialContest,
)
from judge.ratings import rate_contest from judge.ratings import rate_contest
from judge.widgets import ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
@ -24,6 +31,7 @@ from judge.widgets import (
AdminSelect2Widget, AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget, HeavyPreviewAdminPageDownWidget,
) )
from judge.views.contests import recalculate_contest_summary_result
class AdminHeavySelect2Widget(AdminHeavySelect2Widget): class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@ -148,6 +156,26 @@ class ContestForm(ModelForm):
) )
class OfficialContestInlineForm(ModelForm):
class Meta:
widgets = {
"category": AdminSelect2Widget,
"location": AdminSelect2Widget,
}
class OfficialContestInline(admin.StackedInline):
fields = (
"category",
"year",
"location",
)
model = OfficialContest
can_delete = True
form = OfficialContestInlineForm
extra = 0
class ContestAdmin(CompareVersionAdmin): class ContestAdmin(CompareVersionAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}), (None, {"fields": ("key", "name", "authors", "curators", "testers")}),
@ -162,6 +190,7 @@ class ContestAdmin(CompareVersionAdmin):
"scoreboard_visibility", "scoreboard_visibility",
"run_pretests_only", "run_pretests_only",
"points_precision", "points_precision",
"rate_limit",
) )
}, },
), ),
@ -221,7 +250,7 @@ class ContestAdmin(CompareVersionAdmin):
"user_count", "user_count",
) )
search_fields = ("key", "name") search_fields = ("key", "name")
inlines = [ContestProblemInline] inlines = [ContestProblemInline, OfficialContestInline]
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ContestForm form = ContestForm
@ -297,15 +326,23 @@ class ContestAdmin(CompareVersionAdmin):
self._rescore(obj.key) self._rescore(obj.key)
self._rescored = True self._rescored = True
if form.changed_data and any(
f in form.changed_data
for f in (
"authors",
"curators",
"testers",
)
):
Contest._author_ids.dirty(obj)
Contest._curator_ids.dirty(obj)
Contest._tester_ids.dirty(obj)
def save_related(self, request, form, formsets, change): def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change) super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model` # Only rescored if we did not already do so in `save_model`
if not self._rescored and any(formset.has_changed() for formset in formsets): if not self._rescored and any(formset.has_changed() for formset in formsets):
self._rescore(form.cleaned_data["key"]) self._rescore(form.cleaned_data["key"])
obj = form.instance
obj.is_organization_private = obj.organizations.count() > 0
obj.is_private = obj.private_contestants.count() > 0
obj.save()
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if not request.user.has_perm("judge.edit_own_contest"): if not request.user.has_perm("judge.edit_own_contest"):
@ -518,3 +555,9 @@ class ContestsSummaryAdmin(admin.ModelAdmin):
list_display = ("key",) list_display = ("key",)
search_fields = ("key", "contests__key") search_fields = ("key", "contests__key")
form = ContestsSummaryForm form = ContestsSummaryForm
def save_model(self, request, obj, form, change):
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
obj.refresh_from_db()
obj.results = recalculate_contest_summary_result(obj)
obj.save()

52
judge/admin/course.py Normal file
View file

@ -0,0 +1,52 @@
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django.forms import ModelForm
from judge.models import Course, CourseRole
from judge.widgets import AdminSelect2MultipleWidget
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
AdminSelect2Widget,
)
class CourseRoleInlineForm(ModelForm):
class Meta:
widgets = {
"user": AdminHeavySelect2Widget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"role": AdminSelect2Widget,
}
class CourseRoleInline(admin.TabularInline):
model = CourseRole
extra = 1
form = CourseRoleInlineForm
class CourseForm(ModelForm):
class Meta:
widgets = {
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2"
),
"about": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("blog_preview")
),
}
class CourseAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}
inlines = [
CourseRoleInline,
]
list_display = ("name", "is_public", "is_open")
search_fields = ("name",)
form = CourseForm

View file

@ -53,7 +53,8 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
class BlogPostForm(ModelForm): class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BlogPostForm, self).__init__(*args, **kwargs) super(BlogPostForm, self).__init__(*args, **kwargs)
self.fields["authors"].widget.can_add_related = False if "authors" in self.fields:
self.fields["authors"].widget.can_add_related = False
class Meta: class Meta:
widgets = { widgets = {

View file

@ -1,8 +1,8 @@
from operator import attrgetter from operator import attrgetter
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin, messages
from django.db import transaction from django.db import transaction, IntegrityError
from django.db.models import Q, Avg, Count from django.db.models import Q, Avg, Count
from django.db.models.aggregates import StdDev from django.db.models.aggregates import StdDev
from django.forms import ModelForm, TextInput from django.forms import ModelForm, TextInput
@ -11,6 +11,7 @@ from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django_ace import AceWidget from django_ace import AceWidget
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin from reversion_compare.admin import CompareVersionAdmin
@ -56,6 +57,16 @@ class ProblemForm(ModelForm):
} }
) )
def clean_code(self):
code = self.cleaned_data.get("code")
if self.instance.pk:
return code
if Problem.objects.filter(code=code).exists():
raise ValidationError(_("A problem with this code already exists."))
return code
def clean(self): def clean(self):
memory_unit = self.cleaned_data.get("memory_unit", "KB") memory_unit = self.cleaned_data.get("memory_unit", "KB")
if memory_unit == "MB": if memory_unit == "MB":
@ -131,6 +142,7 @@ class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit model = LanguageLimit
fields = ("language", "time_limit", "memory_limit", "memory_unit") fields = ("language", "time_limit", "memory_limit", "memory_unit")
form = LanguageLimitInlineForm form = LanguageLimitInlineForm
extra = 0
class LanguageTemplateInlineForm(ModelForm): class LanguageTemplateInlineForm(ModelForm):
@ -145,6 +157,7 @@ class LanguageTemplateInline(admin.TabularInline):
model = LanguageTemplate model = LanguageTemplate
fields = ("language", "source") fields = ("language", "source")
form = LanguageTemplateInlineForm form = LanguageTemplateInlineForm
extra = 0
class ProblemSolutionForm(ModelForm): class ProblemSolutionForm(ModelForm):
@ -370,8 +383,6 @@ class ProblemAdmin(CompareVersionAdmin):
super().save_related(request, form, formsets, change) super().save_related(request, form, formsets, change)
obj = form.instance obj = form.instance
obj.curators.add(request.profile) obj.curators.add(request.profile)
obj.is_organization_private = obj.organizations.count() > 0
obj.save()
if "curators" in form.changed_data or "authors" in form.changed_data: if "curators" in form.changed_data or "authors" in form.changed_data:
del obj.editor_ids del obj.editor_ids

View file

@ -6,7 +6,7 @@ from reversion.admin import VersionAdmin
from django.contrib.auth.admin import UserAdmin as OldUserAdmin from django.contrib.auth.admin import UserAdmin as OldUserAdmin
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import Profile from judge.models import Profile, ProfileInfo
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
@ -54,6 +54,13 @@ class TimezoneFilter(admin.SimpleListFilter):
return queryset.filter(timezone=self.value()) return queryset.filter(timezone=self.value())
class ProfileInfoInline(admin.StackedInline):
model = ProfileInfo
can_delete = False
verbose_name_plural = "profile info"
fk_name = "profile"
class ProfileAdmin(VersionAdmin): class ProfileAdmin(VersionAdmin):
fields = ( fields = (
"user", "user",
@ -63,15 +70,12 @@ class ProfileAdmin(VersionAdmin):
"timezone", "timezone",
"language", "language",
"ace_theme", "ace_theme",
"math_engine",
"last_access", "last_access",
"ip", "ip",
"mute", "mute",
"is_unlisted", "is_unlisted",
"is_banned_problem_voting",
"notes", "notes",
"is_totp_enabled", "is_totp_enabled",
"user_script",
"current_contest", "current_contest",
) )
readonly_fields = ("user",) readonly_fields = ("user",)
@ -92,6 +96,7 @@ class ProfileAdmin(VersionAdmin):
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ProfileForm form = ProfileForm
inlines = (ProfileInfoInline,)
def get_queryset(self, request): def get_queryset(self, request):
return super(ProfileAdmin, self).get_queryset(request).select_related("user") return super(ProfileAdmin, self).get_queryset(request).select_related("user")
@ -160,15 +165,6 @@ class ProfileAdmin(VersionAdmin):
recalculate_points.short_description = _("Recalculate scores") recalculate_points.short_description = _("Recalculate scores")
def get_form(self, request, obj=None, **kwargs):
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
if "user_script" in form.base_fields:
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
form.base_fields["user_script"].widget = AceWidget(
"javascript", request.profile.ace_theme
)
return form
class UserAdmin(OldUserAdmin): class UserAdmin(OldUserAdmin):
# Customize the fieldsets for adding and editing users # Customize the fieldsets for adding and editing users

View file

@ -56,3 +56,11 @@ class ProblemTypeAdmin(admin.ModelAdmin):
[o.pk for o in obj.problem_set.all()] if obj else [] [o.pk for o in obj.problem_set.all()] if obj else []
) )
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs) return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
class OfficialContestCategoryAdmin(admin.ModelAdmin):
fields = ("name",)
class OfficialContestLocationAdmin(admin.ModelAdmin):
fields = ("name",)

View file

@ -12,7 +12,7 @@ class JudgeAppConfig(AppConfig):
# OPERATIONS MAY HAVE SIDE EFFECTS. # OPERATIONS MAY HAVE SIDE EFFECTS.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED. # DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from . import signals, jinja2 # noqa: F401, imported for side effects from . import models, signals, jinja2 # noqa: F401, imported for side effects
from django.contrib.flatpages.models import FlatPage from django.contrib.flatpages.models import FlatPage
from django.contrib.flatpages.admin import FlatPageAdmin from django.contrib.flatpages.admin import FlatPageAdmin

48
judge/authentication.py Normal file
View file

@ -0,0 +1,48 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.views import PasswordChangeView
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
class CustomModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
# Check if the username is an email
user = User.objects.get(username=username)
except User.DoesNotExist:
# If the username is not an email, try authenticating with the username field
user = User.objects.filter(email=username).first()
if user and user.check_password(password):
return user
class CustomPasswordChangeForm(PasswordChangeForm):
def __init__(self, *args, **kwargs):
super(CustomPasswordChangeForm, self).__init__(*args, **kwargs)
if not self.user.has_usable_password():
self.fields.pop("old_password")
def clean_old_password(self):
if "old_password" not in self.cleaned_data:
return
return super(CustomPasswordChangeForm, self).clean_old_password()
def clean(self):
cleaned_data = super(CustomPasswordChangeForm, self).clean()
if "old_password" not in self.cleaned_data and not self.errors:
cleaned_data["old_password"] = ""
return cleaned_data
class CustomPasswordChangeView(PasswordChangeView):
form_class = CustomPasswordChangeForm
success_url = reverse_lazy("password_change_done")
template_name = "registration/password_change_form.html"
def get_form_kwargs(self):
kwargs = super(CustomPasswordChangeView, self).get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs

View file

@ -24,6 +24,8 @@ from judge.models import (
Submission, Submission,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.bridge.utils import VanishedSubmission
from judge.caching import cache_wrapper
logger = logging.getLogger("judge.bridge") logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge") json_log = logging.getLogger("judge.json.bridge")
@ -65,9 +67,8 @@ class JudgeHandler(ZlibPacketHandler):
self._working = False self._working = False
self._working_data = {} self._working_data = {}
self._no_response_job = None self._no_response_job = None
self._problems = []
self.executors = {} self.executors = {}
self.problems = {} self.problems = set()
self.latency = None self.latency = None
self.time_delta = None self.time_delta = None
self.load = 1e100 self.load = 1e100
@ -139,11 +140,52 @@ class JudgeHandler(ZlibPacketHandler):
) )
return result return result
def _update_supported_problems(self, problem_packet):
# problem_packet is a dict {code: mtimes} from judge-server
self.problems = set(p for p, _ in problem_packet)
def _update_judge_problems(self):
chunk_size = 500
target_problem_codes = self.problems
current_problems = _get_judge_problems(self.judge)
updated = False
problems_to_add = list(target_problem_codes - current_problems)
problems_to_remove = list(current_problems - target_problem_codes)
if problems_to_add:
for i in range(0, len(problems_to_add), chunk_size):
chunk = problems_to_add[i : i + chunk_size]
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
"id", flat=True
)
if not problem_ids:
continue
logger.info("%s: Add %d problems", self.name, len(problem_ids))
self.judge.problems.add(*problem_ids)
updated = True
if problems_to_remove:
for i in range(0, len(problems_to_remove), chunk_size):
chunk = problems_to_remove[i : i + chunk_size]
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
"id", flat=True
)
if not problem_ids:
continue
logger.info("%s: Remove %d problems", self.name, len(problem_ids))
self.judge.problems.remove(*problem_ids)
updated = True
if updated:
_get_judge_problems.dirty(self.judge)
def _connected(self): def _connected(self):
judge = self.judge = Judge.objects.get(name=self.name) judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now() judge.start_time = timezone.now()
judge.online = True judge.online = True
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys()))) self._update_judge_problems()
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys()))) judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
# Delete now in case we somehow crashed and left some over from the last connection # Delete now in case we somehow crashed and left some over from the last connection
@ -178,6 +220,8 @@ class JudgeHandler(ZlibPacketHandler):
def _disconnected(self): def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False) Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete() RuntimeVersion.objects.filter(judge=self.judge).delete()
self.judge.problems.clear()
_get_judge_problems.dirty(self.judge)
def _update_ping(self): def _update_ping(self):
try: try:
@ -208,8 +252,7 @@ class JudgeHandler(ZlibPacketHandler):
return return
self.timeout = 60 self.timeout = 60
self._problems = packet["problems"] self._update_supported_problems(packet["problems"])
self.problems = dict(self._problems)
self.executors = packet["executors"] self.executors = packet["executors"]
self.name = packet["id"] self.name = packet["id"]
@ -310,6 +353,9 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source): def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id) data = self.get_related_submission_data(id)
if not data:
self._update_internal_error_submission(id, "Submission vanished")
raise VanishedSubmission()
self._working = id self._working = id
self._working_data = { self._working_data = {
"problem": problem, "problem": problem,
@ -434,14 +480,12 @@ class JudgeHandler(ZlibPacketHandler):
def on_supported_problems(self, packet): def on_supported_problems(self, packet):
logger.info("%s: Updated problem list", self.name) logger.info("%s: Updated problem list", self.name)
self._problems = packet["problems"] self._update_supported_problems(packet["problems"])
self.problems = dict(self._problems)
if not self.working: if not self.working:
self.judges.update_problems(self) self.judges.update_problems(self)
self.judge.problems.set( self._update_judge_problems()
Problem.objects.filter(code__in=list(self.problems.keys()))
)
json_log.info( json_log.info(
self._make_json_log(action="update-problems", count=len(self.problems)) self._make_json_log(action="update-problems", count=len(self.problems))
) )
@ -658,8 +702,11 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet) self._free_self(packet)
id = packet["submission-id"] id = packet["submission-id"]
self._update_internal_error_submission(id, packet["message"])
def _update_internal_error_submission(self, id, message):
if Submission.objects.filter(id=id).update( if Submission.objects.filter(id=id).update(
status="IE", result="IE", error=packet["message"] status="IE", result="IE", error=message
): ):
event.post( event.post(
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"} "sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
@ -667,9 +714,9 @@ class JudgeHandler(ZlibPacketHandler):
self._post_update_submission(id, "internal-error", done=True) self._post_update_submission(id, "internal-error", done=True)
json_log.info( json_log.info(
self._make_json_log( self._make_json_log(
packet, sub=id,
action="internal-error", action="internal-error",
message=packet["message"], message=message,
finish=True, finish=True,
result="IE", result="IE",
) )
@ -678,10 +725,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id) logger.warning("Unknown submission: %s", id)
json_log.error( json_log.error(
self._make_json_log( self._make_json_log(
packet, sub=id,
action="internal-error", action="internal-error",
info="unknown submission", info="unknown submission",
message=packet["message"], message=message,
finish=True, finish=True,
result="IE", result="IE",
) )
@ -912,3 +959,8 @@ class JudgeHandler(ZlibPacketHandler):
def on_cleanup(self): def on_cleanup(self):
db.connection.close() db.connection.close()
@cache_wrapper(prefix="gjp", timeout=3600)
def _get_judge_problems(judge):
return set(judge.problems.values_list("code", flat=True))

View file

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

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

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

View file

@ -5,6 +5,8 @@ from django.core.handlers.wsgi import WSGIRequest
import hashlib import hashlib
from judge.logging import log_debug
MAX_NUM_CHAR = 50 MAX_NUM_CHAR = 50
NONE_RESULT = "__None__" NONE_RESULT = "__None__"
@ -26,7 +28,7 @@ def filter_args(args_list):
l0_cache = caches["l0"] if "l0" in caches else None l0_cache = caches["l0"] if "l0" in caches else None
def cache_wrapper(prefix, timeout=None): def cache_wrapper(prefix, timeout=None, expected_type=None):
def get_key(func, *args, **kwargs): def get_key(func, *args, **kwargs):
args_list = list(args) args_list = list(args)
signature_args = list(signature(func).parameters.keys()) signature_args = list(signature(func).parameters.keys())
@ -40,7 +42,10 @@ def cache_wrapper(prefix, timeout=None):
def _get(key): def _get(key):
if not l0_cache: if not l0_cache:
return cache.get(key) return cache.get(key)
return l0_cache.get(key) or cache.get(key) result = l0_cache.get(key)
if result is None:
result = cache.get(key)
return result
def _set_l0(key, value): def _set_l0(key, value):
if l0_cache: if l0_cache:
@ -51,18 +56,33 @@ def cache_wrapper(prefix, timeout=None):
cache.set(key, value, timeout) cache.set(key, value, timeout)
def decorator(func): def decorator(func):
def _validate_type(cache_key, result):
if expected_type and not isinstance(result, expected_type):
data = {
"function": f"{func.__module__}.{func.__qualname__}",
"result": str(result)[:30],
"expected_type": expected_type,
"type": type(result),
"key": cache_key,
}
log_debug("invalid_key", data)
return False
return True
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs) cache_key = get_key(func, *args, **kwargs)
result = _get(cache_key) result = _get(cache_key)
if result is not None: if result is not None and _validate_type(cache_key, result):
_set_l0(cache_key, result) _set_l0(cache_key, result)
if result == NONE_RESULT: if type(result) == str and result == NONE_RESULT:
result = None result = None
return result return result
result = func(*args, **kwargs) result = func(*args, **kwargs)
if result is None: if result is None:
result = NONE_RESULT cache_result = NONE_RESULT
_set(cache_key, result, timeout) else:
cache_result = result
_set(cache_key, cache_result, timeout)
return result return result
def dirty(*args, **kwargs): def dirty(*args, **kwargs):
@ -71,7 +91,26 @@ def cache_wrapper(prefix, timeout=None):
if l0_cache: if l0_cache:
l0_cache.delete(cache_key) l0_cache.delete(cache_key)
def prefetch_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
results = cache.get_many(keys)
for key, result in results.items():
if result is not None:
_set_l0(key, result)
def dirty_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
cache.delete_many(keys)
if l0_cache:
l0_cache.delete_many(keys)
wrapper.dirty = dirty wrapper.dirty = dirty
wrapper.prefetch_multi = prefetch_multi
wrapper.dirty_multi = dirty_multi
return wrapper return wrapper

View file

@ -1,233 +0,0 @@
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Count, FilteredRelation, Q
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.forms import ModelForm
from django.http import (
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
Http404,
)
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.generic import View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from reversion import revisions
from reversion.models import Revision, Version
from judge.dblock import LockModel
from judge.models import Comment, Notification
from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text
from judge.models.notification import make_notification
DEFAULT_OFFSET = 10
def _get_html_link_notification(comment):
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
def add_mention_notifications(comment):
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
link = _get_html_link_notification(comment)
make_notification(users_mentioned, "Mention", link, comment.author)
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ["body", "parent"]
widgets = {
"parent": forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets["body"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("comment_preview"),
preview_timeout=1000,
hide_preview_button=True,
)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_("Your part is silent, little toad."))
elif (
not self.request.user.is_staff
and not profile.submission_set.filter(
points=F("problem__points")
).exists()
):
raise ValidationError(
_(
"You need to have solved at least one problem "
"before your voice can be heard."
)
)
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"):
return False
return (
self.request.in_contest
and self.request.participation.contest.use_clarifications
)
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get("parent")
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not self.object.comments.filter(hidden=False, id=parent).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.linked_object = self.object
with LockModel(
write=(Comment, Revision, Version), read=(ContentType,)
), revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_("Posted comment"))
comment.save()
# add notification for reply
comment_notif_link = _get_html_link_notification(comment)
if comment.parent and comment.parent.author != comment.author:
make_notification(
[comment.parent.author], "Reply", comment_notif_link, comment.author
)
# add notification for page authors
page_authors = comment.linked_object.authors.all()
make_notification(
page_authors, "Comment", comment_notif_link, comment.author
)
add_mention_notifications(comment)
return HttpResponseRedirect(comment.get_absolute_url())
context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context)
def get(self, request, *args, **kwargs):
target_comment = None
self.object = self.get_object()
if "comment-id" in request.GET:
try:
comment_id = int(request.GET["comment-id"])
comment_obj = Comment.objects.get(id=comment_id)
except (Comment.DoesNotExist, ValueError):
raise Http404
if comment_obj.linked_object != self.object:
raise Http404
target_comment = comment_obj.get_root()
return self.render_to_response(
self.get_context_data(
object=self.object,
target_comment=target_comment,
comment_form=CommentForm(request, initial={"parent": None}),
)
)
def _get_queryset(self, target_comment):
if target_comment:
queryset = target_comment.get_descendants(include_self=True)
queryset = (
queryset.select_related("author__user")
.filter(hidden=False)
.defer("author__about")
.annotate(revisions=Count("versions", distinct=True))
)
else:
queryset = self.object.comments
queryset = queryset.filter(parent=None, hidden=False)
queryset = (
queryset.select_related("author__user")
.defer("author__about")
.filter(hidden=False)
.annotate(
count_replies=Count("replies", distinct=True),
revisions=Count("versions", distinct=True),
)[:DEFAULT_OFFSET]
)
if self.request.user.is_authenticated:
profile = self.request.profile
queryset = queryset.annotate(
my_vote=FilteredRelation(
"votes", condition=Q(votes__voter_id=profile.id)
),
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
return queryset
def get_context_data(self, target_comment=None, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = self._get_queryset(target_comment)
comment_count = self.object.comments.filter(parent=None, hidden=False).count()
context["target_comment"] = -1
if target_comment != None:
context["target_comment"] = target_comment.id
if self.request.user.is_authenticated:
context["is_new_user"] = (
not self.request.user.is_staff
and not self.request.profile.submission_set.filter(
points=F("problem__points")
).exists()
)
context["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked()
context["comment_list"] = list(queryset)
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
if queryset.exists():
context["comment_root_id"] = context["comment_list"][0].id
else:
context["comment_root_id"] = 0
context["comment_parent_none"] = 1
if target_comment != None:
context["offset"] = 0
context["comment_more"] = comment_count - 1
else:
context["offset"] = DEFAULT_OFFSET
context["comment_more"] = comment_count - DEFAULT_OFFSET
context["limit"] = DEFAULT_OFFSET
context["comment_count"] = comment_count
context["profile"] = self.request.profile
return context

View file

@ -109,6 +109,8 @@ class BaseContestFormat(metaclass=ABCMeta):
) )
for result in queryset: for result in queryset:
problem = str(result["problem_id"]) problem = str(result["problem_id"])
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
continue
if format_data.get(problem): if format_data.get(problem):
is_after_freeze = ( is_after_freeze = (
self.contest.freeze_after self.contest.freeze_after

View file

@ -0,0 +1,22 @@
from django.utils.translation import gettext_lazy as _, ngettext
def custom_trans():
return [
# Password reset
ngettext(
"This password is too short. It must contain at least %(min_length)d character.",
"This password is too short. It must contain at least %(min_length)d characters.",
0,
),
ngettext(
"Your password must contain at least %(min_length)d character.",
"Your password must contain at least %(min_length)d characters.",
0,
),
_("The two password fields didnt match."),
_("Your password cant be entirely numeric."),
# Navbar
_("Bug Report"),
_("Courses"),
]

View file

@ -1,120 +0,0 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.syndication.views import Feed
from django.core.cache import cache
from django.utils import timezone
from django.utils.feedgenerator import Atom1Feed
from judge.jinja2.markdown import markdown
from judge.models import BlogPost, Comment, Problem
import re
# https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
def escape_xml_illegal_chars(val, replacement="?"):
_illegal_xml_chars_RE = re.compile(
"[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]"
)
return _illegal_xml_chars_RE.sub(replacement, val)
class ProblemFeed(Feed):
title = "Recently Added %s Problems" % settings.SITE_NAME
link = "/"
description = (
"The latest problems added on the %s website" % settings.SITE_LONG_NAME
)
def items(self):
return (
Problem.objects.filter(is_public=True, is_organization_private=False)
.defer("description")
.order_by("-date", "-id")[:25]
)
def item_title(self, problem):
return problem.name
def item_description(self, problem):
key = "problem_feed:%d" % problem.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(problem.description))[:500] + "..."
desc = escape_xml_illegal_chars(desc)
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, problem):
return problem.date
item_updateddate = item_pubdate
class AtomProblemFeed(ProblemFeed):
feed_type = Atom1Feed
subtitle = ProblemFeed.description
class CommentFeed(Feed):
title = "Latest %s Comments" % settings.SITE_NAME
link = "/"
description = "The latest comments on the %s website" % settings.SITE_LONG_NAME
def items(self):
return Comment.most_recent(AnonymousUser(), 25)
def item_title(self, comment):
return "%s -> %s" % (comment.author.user.username, comment.page_title)
def item_description(self, comment):
key = "comment_feed:%d" % comment.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(comment.body))
desc = escape_xml_illegal_chars(desc)
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, comment):
return comment.time
item_updateddate = item_pubdate
class AtomCommentFeed(CommentFeed):
feed_type = Atom1Feed
subtitle = CommentFeed.description
class BlogFeed(Feed):
title = "Latest %s Blog Posts" % settings.SITE_NAME
link = "/"
description = "The latest blog posts from the %s" % settings.SITE_LONG_NAME
def items(self):
return BlogPost.objects.filter(
visible=True, publish_on__lte=timezone.now()
).order_by("-sticky", "-publish_on")
def item_title(self, post):
return post.title
def item_description(self, post):
key = "blog_feed:%d" % post.id
summary = cache.get(key)
if summary is None:
summary = str(markdown(post.summary or post.content))
summary = escape_xml_illegal_chars(summary)
cache.set(key, summary, 86400)
return summary
def item_pubdate(self, post):
return post.publish_on
item_updateddate = item_pubdate
class AtomBlogFeed(BlogFeed):
feed_type = Atom1Feed
subtitle = BlogFeed.description

View file

@ -8,7 +8,6 @@
"ip": "10.0.2.2", "ip": "10.0.2.2",
"language": 1, "language": 1,
"last_access": "2017-12-02T08:57:10.093Z", "last_access": "2017-12-02T08:57:10.093Z",
"math_engine": "auto",
"mute": false, "mute": false,
"organizations": [ "organizations": [
1 1
@ -18,8 +17,7 @@
"problem_count": 0, "problem_count": 0,
"rating": null, "rating": null,
"timezone": "America/Toronto", "timezone": "America/Toronto",
"user": 1, "user": 1
"user_script": ""
}, },
"model": "judge.profile", "model": "judge.profile",
"pk": 1 "pk": 1

View file

@ -29,6 +29,7 @@ from django_ace import AceWidget
from judge.models import ( from judge.models import (
Contest, Contest,
Language, Language,
TestFormatterModel,
Organization, Organization,
PrivateMessage, PrivateMessage,
Problem, Problem,
@ -37,11 +38,12 @@ from judge.models import (
Submission, Submission,
BlogPost, BlogPost,
ContestProblem, ContestProblem,
TestFormatterModel,
ProfileInfo,
) )
from judge.widgets import ( from judge.widgets import (
HeavyPreviewPageDownWidget, HeavyPreviewPageDownWidget,
MathJaxPagedownWidget,
PagedownWidget, PagedownWidget,
Select2MultipleWidget, Select2MultipleWidget,
Select2Widget, Select2Widget,
@ -50,6 +52,7 @@ from judge.widgets import (
Select2MultipleWidget, Select2MultipleWidget,
DateTimePickerWidget, DateTimePickerWidget,
ImageWidget, ImageWidget,
DatePickerWidget,
) )
@ -68,6 +71,17 @@ class UserForm(ModelForm):
] ]
class ProfileInfoForm(ModelForm):
class Meta:
model = ProfileInfo
fields = ["tshirt_size", "date_of_birth", "address"]
widgets = {
"tshirt_size": Select2Widget(attrs={"style": "width:100%"}),
"date_of_birth": DatePickerWidget,
"address": forms.TextInput(attrs={"style": "width:100%"}),
}
class ProfileForm(ModelForm): class ProfileForm(ModelForm):
class Meta: class Meta:
model = Profile model = Profile
@ -76,12 +90,10 @@ class ProfileForm(ModelForm):
"timezone", "timezone",
"language", "language",
"ace_theme", "ace_theme",
"user_script",
"profile_image", "profile_image",
"css_background", "css_background",
] ]
widgets = { widgets = {
"user_script": AceWidget(theme="github"),
"timezone": Select2Widget(attrs={"style": "width:200px"}), "timezone": Select2Widget(attrs={"style": "width:200px"}),
"language": Select2Widget(attrs={"style": "width:200px"}), "language": Select2Widget(attrs={"style": "width:200px"}),
"ace_theme": Select2Widget(attrs={"style": "width:200px"}), "ace_theme": Select2Widget(attrs={"style": "width:200px"}),
@ -89,11 +101,6 @@ class ProfileForm(ModelForm):
"css_background": forms.TextInput(), "css_background": forms.TextInput(),
} }
has_math_config = bool(settings.MATHOID_URL)
if has_math_config:
fields.append("math_engine")
widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"})
if HeavyPreviewPageDownWidget is not None: if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget( widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("profile_preview"), preview=reverse_lazy("profile_preview"),
@ -301,8 +308,8 @@ class EditOrganizationContestForm(ModelForm):
"hide_problem_tags", "hide_problem_tags",
"public_scoreboard", "public_scoreboard",
"scoreboard_visibility", "scoreboard_visibility",
"run_pretests_only",
"points_precision", "points_precision",
"rate_limit",
"description", "description",
"og_image", "og_image",
"logo_override_image", "logo_override_image",
@ -412,13 +419,15 @@ class NewMessageForm(ModelForm):
fields = ["title", "content"] fields = ["title", "content"]
widgets = {} widgets = {}
if PagedownWidget is not None: if PagedownWidget is not None:
widgets["content"] = MathJaxPagedownWidget() widgets["content"] = PagedownWidget()
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CustomAuthenticationForm, self).__init__(*args, **kwargs) super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
self.fields["username"].widget.attrs.update({"placeholder": _("Username")}) self.fields["username"].widget.attrs.update(
{"placeholder": _("Username/Email")}
)
self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2") self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
@ -566,3 +575,9 @@ class ContestProblemFormSet(
) )
): ):
model = ContestProblem model = ContestProblem
class TestFormatterForm(ModelForm):
class Meta:
model = TestFormatterModel
fields = ["file"]

View file

@ -1,44 +1,13 @@
from django.utils.html import escape, mark_safe from django.utils.html import escape, mark_safe
from judge.markdown import markdown
__all__ = ["highlight_code"] __all__ = ["highlight_code"]
def _make_pre_code(code): def highlight_code(code, language, linenos=True, title=None):
return mark_safe("<pre>" + escape(code) + "</pre>") linenos_option = 'linenums="1"' if linenos else ""
title_option = f'title="{title}"' if title else ""
options = f"{{.{language} {linenos_option} {title_option}}}"
value = f"```{options}\n{code}\n```\n"
try: return mark_safe(markdown(value))
import pygments
import pygments.lexers
import pygments.formatters
import pygments.util
except ImportError:
def highlight_code(code, language, cssclass=None):
return _make_pre_code(code)
else:
def highlight_code(code, language, cssclass="codehilite", linenos=True):
try:
lexer = pygments.lexers.get_lexer_by_name(language)
except pygments.util.ClassNotFound:
return _make_pre_code(code)
if linenos:
return mark_safe(
pygments.highlight(
code,
lexer,
pygments.formatters.HtmlFormatter(
cssclass=cssclass, linenos="table", wrapcode=True
),
)
)
return mark_safe(
pygments.highlight(
code,
lexer,
pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True),
)
)

View file

@ -22,6 +22,7 @@ from . import (
social, social,
spaceless, spaceless,
timedelta, timedelta,
comment,
) )
from . import registry from . import registry

12
judge/jinja2/comment.py Normal file
View file

@ -0,0 +1,12 @@
from . import registry
from django.contrib.contenttypes.models import ContentType
from judge.models.comment import get_visible_comment_count
from judge.caching import cache_wrapper
@registry.function
def comment_count(obj):
content_type = ContentType.objects.get_for_model(obj)
return get_visible_comment_count(content_type, obj.pk)

View file

@ -23,5 +23,5 @@ registry.filter(localtime_wrapper(time))
@registry.function @registry.function
@registry.render_with("widgets/relative-time.html") @registry.render_with("widgets/relative-time.html")
def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("on {time}")): def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("{time}")):
return {"time": time, "format": format, "rel_format": rel, "abs_format": abs} return {"time": time, "format": format, "rel_format": rel, "abs_format": abs}

View file

@ -10,10 +10,11 @@ from . import registry
@registry.function @registry.function
def gravatar(profile, size=80, default=None, profile_image=None, email=None): def gravatar(profile, size=80, default=None, profile_image=None, email=None):
if profile_image: if profile and not profile.is_muted:
return profile_image if profile_image:
if profile and profile.profile_image_url: return profile_image
return profile.profile_image_url if profile and profile.profile_image_url:
return profile.profile_image_url
if profile: if profile:
email = email or profile.email email = email or profile.email
if default is None: if default is None:

View file

@ -1,112 +1,7 @@
from .. import registry from .. import registry
import markdown as _markdown from judge.markdown import markdown as _markdown
import bleach
from django.utils.html import escape
from bs4 import BeautifulSoup
from pymdownx import superfences
EXTENSIONS = [
"pymdownx.arithmatex",
"pymdownx.magiclink",
"pymdownx.betterem",
"pymdownx.details",
"pymdownx.emoji",
"pymdownx.inlinehilite",
"pymdownx.superfences",
"pymdownx.tasklist",
"markdown.extensions.footnotes",
"markdown.extensions.attr_list",
"markdown.extensions.def_list",
"markdown.extensions.tables",
"markdown.extensions.admonition",
"nl2br",
"mdx_breakless_lists",
]
EXTENSION_CONFIGS = {
"pymdownx.superfences": {
"custom_fences": [
{
"name": "sample",
"class": "no-border",
"format": superfences.fence_code_format,
}
]
},
}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
"img",
"center",
"iframe",
"div",
"span",
"table",
"tr",
"td",
"th",
"tr",
"pre",
"code",
"p",
"hr",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"thead",
"tbody",
"sup",
"dl",
"dt",
"dd",
"br",
"details",
"summary",
]
ALLOWED_ATTRS = ["src", "width", "height", "href", "class", "open"]
@registry.filter @registry.filter
def markdown(value, lazy_load=False): def markdown(value, lazy_load=False):
extensions = EXTENSIONS return _markdown(value, lazy_load)
html = _markdown.markdown(
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
)
# Don't clean mathjax
hash_script_tag = {}
soup = BeautifulSoup(html, "html.parser")
for script_tag in soup.find_all("script"):
allow_math_types = ["math/tex", "math/tex; mode=display"]
if script_tag.attrs.get("type", False) in allow_math_types:
hash_script_tag[str(hash(str(script_tag)))] = str(script_tag)
for hashed_tag in hash_script_tag:
tag = hash_script_tag[hashed_tag]
html = html.replace(tag, hashed_tag)
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
for hashed_tag in hash_script_tag:
tag = hash_script_tag[hashed_tag]
html = html.replace(hashed_tag, tag)
if not html:
html = escape(value)
if lazy_load:
soup = BeautifulSoup(html, features="html.parser")
for img in soup.findAll("img"):
if img.get("src"):
img["data-src"] = img["src"]
img["src"] = ""
for img in soup.findAll("iframe"):
if img.get("src"):
img["data-src"] = img["src"]
img["src"] = ""
html = str(soup)
return '<div class="md-typeset">%s</div>' % html

View file

@ -155,16 +155,16 @@ def item_title(item):
@registry.function @registry.function
@registry.render_with("user/link.html") @registry.render_with("user/link.html")
def link_user(user): def link_user(user, show_image=False):
if isinstance(user, Profile): if isinstance(user, Profile):
profile = user profile = user
elif isinstance(user, AbstractUser): elif isinstance(user, AbstractUser):
profile = user.profile profile = user.profile
elif type(user).__name__ == "ContestRankingProfile": elif isinstance(user, int):
profile = user profile = Profile(id=user)
else: else:
raise ValueError("Expected profile or user, got %s" % (type(user),)) raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"profile": profile} return {"profile": profile, "show_image": show_image}
@registry.function @registry.function

View file

@ -48,5 +48,9 @@ for name, template, url_func in SHARES:
@registry.function @registry.function
def recaptcha_init(language=None): def recaptcha_init(language=None):
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render( return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
{"explicit": False, "language": language} {
"explicit": False,
"language": language,
"recaptcha_host": "https://google.com",
}
) )

View file

@ -1,7 +1,12 @@
import logging import logging
error_log = logging.getLogger("judge.errors") error_log = logging.getLogger("judge.errors")
debug_log = logging.getLogger("judge.debug")
def log_exception(msg): def log_exception(msg):
error_log.exception(msg) error_log.exception(msg)
def log_debug(category, data):
debug_log.info(f"{category}: {data}")

View file

@ -89,14 +89,13 @@ class Command(BaseCommand):
if trans is None if trans is None
else trans.description, else trans.description,
"url": "", "url": "",
"math_engine": maker.math_engine,
} }
) )
.replace('"//', '"https://') .replace('"//', '"https://')
.replace("'//", "'https://") .replace("'//", "'https://")
) )
maker.title = problem_name maker.title = problem_name
for file in ("style.css", "pygment-github.css", "mathjax3_config.js"): for file in "style.css":
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make(debug=True) maker.make(debug=True)
if not maker.success: if not maker.success:

149
judge/markdown.py Normal file
View file

@ -0,0 +1,149 @@
import markdown as _markdown
import bleach
from django.utils.html import escape
from bs4 import BeautifulSoup
from pymdownx import superfences
from django.conf import settings
from urllib.parse import urlparse
from judge.markdown_extensions import YouTubeExtension, EmoticonExtension
EXTENSIONS = [
"pymdownx.arithmatex",
"pymdownx.magiclink",
"pymdownx.betterem",
"pymdownx.details",
"pymdownx.emoji",
"pymdownx.inlinehilite",
"pymdownx.superfences",
"pymdownx.highlight",
"pymdownx.tasklist",
"markdown.extensions.footnotes",
"markdown.extensions.attr_list",
"markdown.extensions.def_list",
"markdown.extensions.tables",
"markdown.extensions.admonition",
"nl2br",
"mdx_breakless_lists",
YouTubeExtension(),
EmoticonExtension(),
]
EXTENSION_CONFIGS = {
"pymdownx.arithmatex": {
"generic": True,
},
"pymdownx.superfences": {
"custom_fences": [
{
"name": "sample",
"class": "no-border",
"format": superfences.fence_code_format,
}
],
},
"pymdownx.highlight": {
"auto_title": True,
"auto_title_map": {
"Text Only": "",
},
},
}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
"img",
"center",
"iframe",
"div",
"span",
"table",
"tr",
"td",
"th",
"tr",
"pre",
"code",
"p",
"hr",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"thead",
"tbody",
"sup",
"dl",
"dt",
"dd",
"br",
"details",
"summary",
]
ALLOWED_ATTRS = [
"src",
"width",
"height",
"href",
"class",
"open",
"title",
"frameborder",
"allow",
"allowfullscreen",
"loading",
]
def _wrap_img_iframe_with_lazy_load(soup):
for img in soup.findAll("img"):
if img.get("src"):
img["loading"] = "lazy"
for img in soup.findAll("iframe"):
if img.get("src"):
img["loading"] = "lazy"
return soup
def _wrap_images_with_featherlight(soup):
for img in soup.findAll("img"):
if img.get("src"):
link = soup.new_tag("a", href=img["src"], **{"data-featherlight": "image"})
img.wrap(link)
return soup
def _open_external_links_in_new_tab(soup):
domain = settings.SITE_DOMAIN.lower()
for a in soup.findAll("a", href=True):
href = a["href"]
if href.startswith("http://") or href.startswith("https://"):
link_domain = urlparse(href).netloc.lower()
if link_domain != domain:
a["target"] = "_blank"
return soup
def markdown(value, lazy_load=False):
extensions = EXTENSIONS
html = _markdown.markdown(
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
)
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
if not html:
html = escape(value)
soup = BeautifulSoup(html, features="html.parser")
if lazy_load:
soup = _wrap_img_iframe_with_lazy_load(soup)
soup = _wrap_images_with_featherlight(soup)
soup = _open_external_links_in_new_tab(soup)
html = str(soup)
return '<div class="md-typeset content-description">%s</div>' % html

View file

@ -0,0 +1,2 @@
from .youtube import YouTubeExtension
from .emoticon import EmoticonExtension

View file

@ -0,0 +1,112 @@
import markdown
from markdown.extensions import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
import re
EMOTICON_EMOJI_MAP = {
":D": "\U0001F603", # Smiling Face with Open Mouth
":)": "\U0001F642", # Slightly Smiling Face
":-)": "\U0001F642", # Slightly Smiling Face with Nose
":(": "\U0001F641", # Slightly Frowning Face
":-(": "\U0001F641", # Slightly Frowning Face with Nose
";)": "\U0001F609", # Winking Face
";-)": "\U0001F609", # Winking Face with Nose
":P": "\U0001F61B", # Face with Tongue
":-P": "\U0001F61B", # Face with Tongue and Nose
":p": "\U0001F61B", # Face with Tongue
":-p": "\U0001F61B", # Face with Tongue and Nose
";P": "\U0001F61C", # Winking Face with Tongue
";-P": "\U0001F61C", # Winking Face with Tongue and Nose
";p": "\U0001F61C", # Winking Face with Tongue
";-p": "\U0001F61C", # Winking Face with Tongue and Nose
":'(": "\U0001F622", # Crying Face
":o": "\U0001F62E", # Face with Open Mouth
":-o": "\U0001F62E", # Face with Open Mouth and Nose
":O": "\U0001F62E", # Face with Open Mouth
":-O": "\U0001F62E", # Face with Open Mouth and Nose
":-0": "\U0001F62E", # Face with Open Mouth and Nose
">:(": "\U0001F620", # Angry Face
">:-(": "\U0001F620", # Angry Face with Nose
">:)": "\U0001F608", # Smiling Face with Horns
">:-)": "\U0001F608", # Smiling Face with Horns and Nose
"XD": "\U0001F606", # Grinning Squinting Face
"xD": "\U0001F606", # Grinning Squinting Face
"B)": "\U0001F60E", # Smiling Face with Sunglasses
"B-)": "\U0001F60E", # Smiling Face with Sunglasses and Nose
"O:)": "\U0001F607", # Smiling Face with Halo
"O:-)": "\U0001F607", # Smiling Face with Halo and Nose
"0:)": "\U0001F607", # Smiling Face with Halo
"0:-)": "\U0001F607", # Smiling Face with Halo and Nose
">:P": "\U0001F92A", # Zany Face (sticking out tongue and winking)
">:-P": "\U0001F92A", # Zany Face with Nose
">:p": "\U0001F92A", # Zany Face (sticking out tongue and winking)
">:-p": "\U0001F92A", # Zany Face with Nose
":/": "\U0001F615", # Confused Face
":-/": "\U0001F615", # Confused Face with Nose
":\\": "\U0001F615", # Confused Face
":-\\": "\U0001F615", # Confused Face with Nose
"3:)": "\U0001F608", # Smiling Face with Horns
"3:-)": "\U0001F608", # Smiling Face with Horns and Nose
"<3": "\u2764\uFE0F", # Red Heart
"</3": "\U0001F494", # Broken Heart
":*": "\U0001F618", # Face Blowing a Kiss
":-*": "\U0001F618", # Face Blowing a Kiss with Nose
";P": "\U0001F61C", # Winking Face with Tongue
";-P": "\U0001F61C",
">:P": "\U0001F61D", # Face with Stuck-Out Tongue and Tightly-Closed Eyes
":-/": "\U0001F615", # Confused Face
":/": "\U0001F615",
":\\": "\U0001F615",
":-\\": "\U0001F615",
":|": "\U0001F610", # Neutral Face
":-|": "\U0001F610",
"8)": "\U0001F60E", # Smiling Face with Sunglasses
"8-)": "\U0001F60E",
"O:)": "\U0001F607", # Smiling Face with Halo
"O:-)": "\U0001F607",
":3": "\U0001F60A", # Smiling Face with Smiling Eyes
"^.^": "\U0001F60A",
"-_-": "\U0001F611", # Expressionless Face
"T_T": "\U0001F62D", # Loudly Crying Face
"T.T": "\U0001F62D",
">.<": "\U0001F623", # Persevering Face
"x_x": "\U0001F635", # Dizzy Face
"X_X": "\U0001F635",
":]": "\U0001F600", # Grinning Face
":[": "\U0001F641", # Slightly Frowning Face
"=]": "\U0001F600",
"=[": "\U0001F641",
"D:<": "\U0001F621", # Pouting Face
"D:": "\U0001F629", # Weary Face
"D=": "\U0001F6AB", # No Entry Sign (sometimes used to denote dismay or frustration)
":'D": "\U0001F602", # Face with Tears of Joy
"D':": "\U0001F625", # Disappointed but Relieved Face
"D8": "\U0001F631", # Face Screaming in Fear
"-.-": "\U0001F644", # Face with Rolling Eyes
"-_-;": "\U0001F612", # Unamused
}
class EmoticonEmojiInlineProcessor(InlineProcessor):
def handleMatch(self, m, data):
emoticon = m.group(1)
emoji = EMOTICON_EMOJI_MAP.get(emoticon, "")
if emoji:
el = etree.Element("span")
el.text = markdown.util.AtomicString(emoji)
el.set("class", "big-emoji")
return el, m.start(0), m.end(0)
else:
return None, m.start(0), m.end(0)
class EmoticonExtension(Extension):
def extendMarkdown(self, md):
emoticon_pattern = (
r"(?:(?<=\s)|^)" # Lookbehind for a whitespace character or the start of the string
r"(" + "|".join(map(re.escape, EMOTICON_EMOJI_MAP.keys())) + r")"
r"(?=\s|$)" # Lookahead for a whitespace character or the end of the string
)
emoticon_processor = EmoticonEmojiInlineProcessor(emoticon_pattern, md)
md.inlinePatterns.register(emoticon_processor, "emoticon_to_emoji", 1)

View file

@ -0,0 +1,36 @@
import markdown
from markdown.inlinepatterns import InlineProcessor
from markdown.extensions import Extension
import xml.etree.ElementTree as etree
YOUTUBE_REGEX = (
r"(https?://)?(www\.)?" "(youtube\.com/watch\?v=|youtu\.be/)" "([\w-]+)(&[\w=]*)?"
)
class YouTubeEmbedProcessor(InlineProcessor):
def handleMatch(self, m, data):
youtube_id = m.group(4)
if not youtube_id:
return None, None, None
# Create an iframe element with the YouTube embed URL
iframe = etree.Element("iframe")
iframe.set("width", "100%")
iframe.set("height", "360")
iframe.set("src", f"https://www.youtube.com/embed/{youtube_id}")
iframe.set("frameborder", "0")
iframe.set("allowfullscreen", "true")
center = etree.Element("center")
center.append(iframe)
# Return the iframe as the element to replace the match, along with the start and end indices
return center, m.start(0), m.end(0)
class YouTubeExtension(Extension):
def extendMarkdown(self, md):
# Create the YouTube link pattern
YOUTUBE_PATTERN = YouTubeEmbedProcessor(YOUTUBE_REGEX, md)
# Register the pattern to apply the YouTubeEmbedProcessor
md.inlinePatterns.register(YOUTUBE_PATTERN, "youtube", 175)

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.18 on 2023-11-24 05:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0173_fulltext"),
]
operations = [
migrations.AddField(
model_name="contestssummary",
name="results",
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name="contest",
name="authors",
field=models.ManyToManyField(
help_text="These users will be able to edit the contest.",
related_name="_judge_contest_authors_+",
to="judge.Profile",
verbose_name="authors",
),
),
migrations.AlterField(
model_name="contest",
name="curators",
field=models.ManyToManyField(
blank=True,
help_text="These users will be able to edit the contest, but will not be listed as authors.",
related_name="_judge_contest_curators_+",
to="judge.Profile",
verbose_name="curators",
),
),
migrations.AlterField(
model_name="contest",
name="testers",
field=models.ManyToManyField(
blank=True,
help_text="These users will be able to view the contest, but not edit it.",
related_name="_judge_contest_testers_+",
to="judge.Profile",
verbose_name="testers",
),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.18 on 2023-11-29 02:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0174_contest_summary_result"),
]
operations = [
migrations.AddIndex(
model_name="profile",
index=models.Index(
fields=["is_unlisted", "performance_points"],
name="judge_profi_is_unli_d4034c_idx",
),
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 3.2.18 on 2023-12-06 01:28
from django.db import migrations, models
# Run this in shell
def migrate_revision(apps, schema_editor):
Comment = apps.get_model("judge", "Comment")
for c in Comment.objects.all():
c.revision_count = c.versions.count()
c.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0175_add_profile_index"),
]
operations = [
migrations.AddField(
model_name="comment",
name="revision_count",
field=models.PositiveIntegerField(default=1),
),
# migrations.RunPython(
# migrate_revision, migrations.RunPython.noop, atomic=True
# ),
]

View file

@ -0,0 +1,35 @@
from django.db import migrations, models
import judge.models.test_formatter
class Migration(migrations.Migration):
dependencies = [
("judge", "0176_comment_revision_count"),
]
operations = [
migrations.CreateModel(
name="TestFormatterModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
blank=True,
null=True,
upload_to=judge.models.test_formatter.test_formatter_path,
verbose_name="testcase file",
),
),
],
)
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2024-01-14 01:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0177_test_formatter"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="user_script",
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2024-01-23 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0178_remove_user_script"),
]
operations = [
migrations.AddIndex(
model_name="submission",
index=models.Index(
fields=["language", "result"], name="judge_submi_languag_874af4_idx"
),
),
]

View file

@ -0,0 +1,78 @@
# Generated by Django 3.2.18 on 2024-02-15 02:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0179_submission_result_lang_index"),
]
operations = [
migrations.CreateModel(
name="CourseLesson",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.TextField(verbose_name="course title")),
("content", models.TextField(verbose_name="course content")),
("order", models.IntegerField(default=0, verbose_name="order")),
("points", models.IntegerField(verbose_name="points")),
],
),
migrations.RemoveField(
model_name="courseresource",
name="course",
),
migrations.RemoveField(
model_name="course",
name="ending_time",
),
migrations.AlterField(
model_name="courserole",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
migrations.DeleteModel(
name="CourseAssignment",
),
migrations.DeleteModel(
name="CourseResource",
),
migrations.AddField(
model_name="courselesson",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
migrations.AddField(
model_name="courselesson",
name="problems",
field=models.ManyToManyField(to="judge.Problem"),
),
migrations.AlterUniqueTogether(
name="courserole",
unique_together={("course", "user")},
),
migrations.AlterField(
model_name="courselesson",
name="problems",
field=models.ManyToManyField(blank=True, to="judge.Problem"),
),
]

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.18 on 2024-02-26 20:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0180_course"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="math_engine",
),
migrations.AlterField(
model_name="course",
name="about",
field=models.TextField(verbose_name="course description"),
),
migrations.AlterField(
model_name="courselesson",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lessons",
to="judge.course",
verbose_name="course",
),
),
migrations.AlterField(
model_name="courselesson",
name="problems",
field=models.ManyToManyField(
blank=True, to="judge.Problem", verbose_name="problem"
),
),
migrations.AlterField(
model_name="courserole",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="course_roles",
to="judge.profile",
verbose_name="user",
),
),
]

View file

@ -0,0 +1,75 @@
# Generated by Django 3.2.18 on 2024-03-19 04:28
from django.db import migrations, models
def migrate_checker(apps, schema_editor):
ProblemData = apps.get_model("judge", "ProblemData")
ProblemTestCase = apps.get_model("judge", "ProblemTestCase")
for p in ProblemData.objects.all():
if p.checker == "customval":
p.checker = "customcpp"
p.save()
for p in ProblemTestCase.objects.all():
if p.checker == "customval":
p.checker = "customcpp"
p.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0181_remove_math_engine"),
]
operations = [
migrations.RunPython(migrate_checker, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name="problemdata",
name="checker",
field=models.CharField(
blank=True,
choices=[
("standard", "Standard"),
("floats", "Floats"),
("floatsabs", "Floats (absolute)"),
("floatsrel", "Floats (relative)"),
("rstripped", "Non-trailing spaces"),
("sorted", "Unordered"),
("identical", "Byte identical"),
("linecount", "Line-by-line"),
("custom", "Custom checker (PY)"),
("customcpp", "Custom checker (CPP)"),
("interact", "Interactive"),
("testlib", "Testlib"),
],
max_length=10,
verbose_name="checker",
),
),
migrations.AlterField(
model_name="problemtestcase",
name="checker",
field=models.CharField(
blank=True,
choices=[
("standard", "Standard"),
("floats", "Floats"),
("floatsabs", "Floats (absolute)"),
("floatsrel", "Floats (relative)"),
("rstripped", "Non-trailing spaces"),
("sorted", "Unordered"),
("identical", "Byte identical"),
("linecount", "Line-by-line"),
("custom", "Custom checker (PY)"),
("customcpp", "Custom checker (CPP)"),
("interact", "Interactive"),
("testlib", "Testlib"),
],
max_length=10,
verbose_name="checker",
),
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.18 on 2024-03-19 04:45
import django.core.validators
from django.db import migrations, models
import judge.models.problem_data
import judge.utils.problem_data
def migrate_checker(apps, schema_editor):
ProblemData = apps.get_model("judge", "ProblemData")
for p in ProblemData.objects.all():
p.custom_checker_cpp = p.custom_validator
p.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0182_rename_customcpp"),
]
operations = [
migrations.AddField(
model_name="problemdata",
name="custom_checker_cpp",
field=models.FileField(
blank=True,
null=True,
storage=judge.utils.problem_data.ProblemDataStorage(),
upload_to=judge.models.problem_data.problem_directory_file,
validators=[
django.core.validators.FileExtensionValidator(
allowed_extensions=["cpp"]
)
],
verbose_name="custom cpp checker file",
),
),
migrations.RunPython(migrate_checker, migrations.RunPython.noop, atomic=True),
migrations.RemoveField(
model_name="problemdata",
name="custom_validator",
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.18 on 2024-03-23 04:07
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0183_rename_custom_checker_cpp"),
]
operations = [
migrations.AddField(
model_name="contest",
name="rate_limit",
field=models.PositiveIntegerField(
blank=True,
help_text="Maximum number of submissions per minute. Leave empty if you don't want rate limit.",
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(5),
],
verbose_name="rate limit",
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2024-04-12 05:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0184_contest_rate_limit"),
]
operations = [
migrations.RenameField(
model_name="organizationprofile",
old_name="users",
new_name="profile",
),
]

View file

@ -0,0 +1,43 @@
# Generated by Django 3.2.18 on 2024-04-12 17:04
from django.db import migrations, models
def truncate_about_text(apps, schema_editor):
Organization = apps.get_model("judge", "Organization")
Profile = apps.get_model("judge", "Profile")
for org in Organization.objects.all():
if len(org.about) > 10000:
org.about = org.about[:10000]
org.save()
for profile in Profile.objects.all():
if profile.about and len(profile.about) > 10000:
profile.about = profile.about[:10000]
profile.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0185_rename_org_profile_colum"),
]
operations = [
migrations.RunPython(truncate_about_text),
migrations.AlterField(
model_name="organization",
name="about",
field=models.CharField(
max_length=10000, verbose_name="organization description"
),
),
migrations.AlterField(
model_name="profile",
name="about",
field=models.CharField(
blank=True, max_length=10000, null=True, verbose_name="self-description"
),
),
]

View file

@ -0,0 +1,69 @@
# Generated by Django 3.2.18 on 2024-04-27 03:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0186_change_about_fields_max_len"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="is_banned_problem_voting",
),
migrations.CreateModel(
name="ProfileInfo",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"tshirt_size",
models.CharField(
blank=True,
choices=[
("S", "Small (S)"),
("M", "Medium (M)"),
("L", "Large (L)"),
("XL", "Extra Large (XL)"),
("XXL", "2 Extra Large (XXL)"),
],
max_length=5,
null=True,
verbose_name="t-shirt size",
),
),
(
"date_of_birth",
models.DateField(
blank=True, null=True, verbose_name="date of birth"
),
),
(
"address",
models.CharField(
blank=True, max_length=255, null=True, verbose_name="address"
),
),
(
"profile",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="info",
to="judge.profile",
verbose_name="profile associated",
),
),
],
),
]

View file

@ -0,0 +1,110 @@
# Generated by Django 3.2.18 on 2024-05-30 04:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0187_profile_info"),
]
operations = [
migrations.CreateModel(
name="OfficialContestCategory",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
max_length=50,
unique=True,
verbose_name="official contest category",
),
),
],
options={
"verbose_name": "official contest category",
"verbose_name_plural": "official contest categories",
},
),
migrations.CreateModel(
name="OfficialContestLocation",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
max_length=50,
unique=True,
verbose_name="official contest location",
),
),
],
options={
"verbose_name": "official contest location",
"verbose_name_plural": "official contest locations",
},
),
migrations.CreateModel(
name="OfficialContest",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("year", models.PositiveIntegerField(verbose_name="year")),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.officialcontestcategory",
verbose_name="contest category",
),
),
(
"contest",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="official",
to="judge.contest",
verbose_name="contest",
),
),
(
"location",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.officialcontestlocation",
verbose_name="contest location",
),
),
],
options={
"verbose_name": "official contest",
"verbose_name_plural": "official contests",
},
),
]

View file

@ -1,7 +1,9 @@
import numpy as np import numpy as np
from django.conf import settings
import os import os
import hashlib
from django.core.cache import cache from django.core.cache import cache
from django.conf import settings
from judge.caching import cache_wrapper from judge.caching import cache_wrapper
@ -12,67 +14,69 @@ class CollabFilter:
# name = 'collab_filter' or 'collab_filter_time' # name = 'collab_filter' or 'collab_filter_time'
def __init__(self, name): def __init__(self, name):
embeddings = np.load( self.embeddings = np.load(
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"), os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
allow_pickle=True, allow_pickle=True,
) )
arr0, arr1 = embeddings.files _, problem_arr = self.embeddings.files
self.name = name self.name = name
self.user_embeddings = embeddings[arr0] self.problem_embeddings = self.embeddings[problem_arr].item()
self.problem_embeddings = embeddings[arr1]
def __str__(self): def __str__(self):
return self.name return self.name
def compute_scores(self, query_embedding, item_embeddings, measure=DOT): def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
"""Computes the scores of the candidates given a query. """Return {id: score}"""
Args:
query_embedding: a vector of shape [k], representing the query embedding.
item_embeddings: a matrix of shape [N, k], such that row i is the embedding
of item i.
measure: a string specifying the similarity measure to be used. Can be
either DOT or COSINE.
Returns:
scores: a vector of shape [N], such that scores[i] is the score of item i.
"""
u = query_embedding u = query_embedding
V = item_embeddings V = np.stack(list(item_embeddings.values()))
if measure == self.COSINE: if measure == self.COSINE:
V = V / np.linalg.norm(V, axis=1, keepdims=True) V = V / np.linalg.norm(V, axis=1, keepdims=True)
u = u / np.linalg.norm(u) u = u / np.linalg.norm(u)
scores = u.dot(V.T) scores = u.dot(V.T)
return scores scores_by_id = {id_: s for id_, s in zip(item_embeddings.keys(), scores)}
return scores_by_id
def _get_embedding_version(self):
first_problem = self.problem_embeddings[0]
array_bytes = first_problem.tobytes()
hash_object = hashlib.sha256(array_bytes)
hash_bytes = hash_object.digest()
return hash_bytes.hex()[:5]
@cache_wrapper(prefix="CFgue", timeout=86400)
def _get_user_embedding(self, user_id, embedding_version):
user_arr, _ = self.embeddings.files
user_embeddings = self.embeddings[user_arr].item()
if user_id not in user_embeddings:
return user_embeddings[0]
return user_embeddings[user_id]
def get_user_embedding(self, user_id):
version = self._get_embedding_version()
return self._get_user_embedding(user_id, version)
@cache_wrapper(prefix="user_recommendations", timeout=3600) @cache_wrapper(prefix="user_recommendations", timeout=3600)
def user_recommendations(self, user, problems, measure=DOT, limit=None): def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
uid = user.id user_embedding = self.get_user_embedding(user_id)
if uid >= len(self.user_embeddings): scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
uid = 0
scores = self.compute_scores(
self.user_embeddings[uid], self.problem_embeddings, measure
)
res = [] # [(score, problem)] res = [] # [(score, problem)]
for pid in problems: for pid in problems:
# pid = problem.id if pid in scores:
if pid < len(scores):
res.append((scores[pid], pid)) res.append((scores[pid], pid))
res.sort(reverse=True, key=lambda x: x[0]) res.sort(reverse=True, key=lambda x: x[0])
res = res[:limit] return res[:limit]
return res
# return a list of pid # return a list of pid
def problem_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 not in self.problem_embeddings:
return [] return []
scores = self.compute_scores( embedding = self.problem_embeddings[pid]
self.problem_embeddings[pid], self.problem_embeddings, measure scores = self.compute_scores(embedding, self.problem_embeddings, measure)
)
res = [] res = []
for p in problemset: for p in problemset:
if p < len(scores): if p in scores:
res.append((scores[p], 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

@ -2,8 +2,6 @@ from reversion import revisions
from judge.models.choices import ( from judge.models.choices import (
ACE_THEMES, ACE_THEMES,
EFFECTIVE_MATH_ENGINES,
MATH_ENGINES_CHOICES,
TIMEZONE, TIMEZONE,
) )
from judge.models.comment import Comment, CommentLock, CommentVote from judge.models.comment import Comment, CommentLock, CommentVote
@ -17,6 +15,9 @@ from judge.models.contest import (
Rating, Rating,
ContestProblemClarification, ContestProblemClarification,
ContestsSummary, ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
OfficialContest,
) )
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread from judge.models.message import PrivateMessage, PrivateMessageThread
@ -45,6 +46,7 @@ from judge.models.profile import (
Profile, Profile,
Friend, Friend,
OrganizationProfile, OrganizationProfile,
ProfileInfo,
) )
from judge.models.runtime import Judge, Language, RuntimeVersion from judge.models.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import ( from judge.models.submission import (
@ -53,12 +55,15 @@ from judge.models.submission import (
SubmissionSource, SubmissionSource,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.models.test_formatter import TestFormatterModel
from judge.models.ticket import Ticket, TicketMessage 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 from judge.models.course import Course, CourseRole, CourseLesson
from judge.models.notification import Notification, NotificationProfile from judge.models.notification import Notification, NotificationProfile
from judge.models.test_formatter import TestFormatterModel
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"]) revisions.register(Problem, follow=["language_limits"])

View file

@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["BookMark"] __all__ = ["BookMark"]
@ -21,12 +22,9 @@ class BookMark(models.Model):
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
linked_object = GenericForeignKey("content_type", "object_id") linked_object = GenericForeignKey("content_type", "object_id")
def get_bookmark(self, user): @cache_wrapper(prefix="BMgb")
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user) def is_bookmarked_by(self, user):
if userqueryset.exists(): return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
return True
else:
return False
class Meta: class Meta:
verbose_name = _("bookmark") verbose_name = _("bookmark")
@ -55,11 +53,22 @@ class MakeBookMark(models.Model):
verbose_name_plural = _("make bookmarks") verbose_name_plural = _("make bookmarks")
@cache_wrapper(prefix="gocb", expected_type=BookMark)
def _get_or_create_bookmark(content_type, object_id):
bookmark, created = BookMark.objects.get_or_create(
content_type=content_type,
object_id=object_id,
)
return bookmark
class Bookmarkable: class Bookmarkable:
def get_or_create_bookmark(self): def get_or_create_bookmark(self):
if self.bookmark.count(): content_type = ContentType.objects.get_for_model(self)
return self.bookmark.first() object_id = self.pk
new_bookmark = BookMark() return _get_or_create_bookmark(content_type, object_id)
new_bookmark.linked_object = self
new_bookmark.save()
return new_bookmark def dirty_bookmark(bookmark, profile):
bookmark.is_bookmarked_by.dirty(bookmark, profile)
_get_or_create_bookmark.dirty(bookmark.content_type, bookmark.object_id)

View file

@ -54,13 +54,3 @@ ACE_THEMES = (
("vibrant_ink", "Vibrant Ink"), ("vibrant_ink", "Vibrant Ink"),
("xcode", "XCode"), ("xcode", "XCode"),
) )
MATH_ENGINES_CHOICES = (
("tex", _("Leave as LaTeX")),
("svg", _("SVG with PNG fallback")),
("mml", _("MathML only")),
("jax", _("MathJax with SVG/PNG fallback")),
("auto", _("Detect best quality")),
)
EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax")

View file

@ -20,6 +20,7 @@ from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution from judge.models.problem import Problem, Solution
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.utils.cachedict import CacheDict from judge.utils.cachedict import CacheDict
from judge.caching import cache_wrapper
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"] __all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
@ -56,6 +57,7 @@ class Comment(MPTTModel):
related_name="replies", related_name="replies",
on_delete=CASCADE, on_delete=CASCADE,
) )
revision_count = models.PositiveIntegerField(default=1)
versions = VersionRelation() versions = VersionRelation()
@ -71,19 +73,14 @@ class Comment(MPTTModel):
@classmethod @classmethod
def most_recent(cls, user, n, batch=None, organization=None): def most_recent(cls, user, n, batch=None, organization=None):
queryset = ( queryset = cls.objects.filter(hidden=False).order_by("-id")
cls.objects.filter(hidden=False)
.select_related("author__user")
.defer("author__about", "body")
.order_by("-id")
)
if organization: if organization:
queryset = queryset.filter(author__in=organization.members.all()) queryset = queryset.filter(author__in=organization.members.all())
problem_access = CacheDict(lambda p: p.is_accessible_by(user)) problem_access = CacheDict(lambda p: p.is_accessible_by(user))
contest_access = CacheDict(lambda c: c.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 b: b.is_accessible_by(user))
if n == -1: if n == -1:
n = len(queryset) n = len(queryset)
@ -118,10 +115,6 @@ class Comment(MPTTModel):
query = Comment.filter(parent=self) query = Comment.filter(parent=self)
return len(query) return len(query)
@cached_property
def get_revisions(self):
return self.versions.count()
@cached_property @cached_property
def page_title(self): def page_title(self):
if isinstance(self.linked_object, Problem): if isinstance(self.linked_object, Problem):
@ -177,3 +170,10 @@ class CommentLock(models.Model):
def __str__(self): def __str__(self):
return str(self.page) return str(self.page)
@cache_wrapper(prefix="gcc")
def get_visible_comment_count(content_type, object_id):
return Comment.objects.filter(
content_type=content_type, object_id=object_id, hidden=False
).count()

View file

@ -2,11 +2,14 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models, transaction from django.db import models, transaction
from django.db.models import CASCADE, Q from django.db.models import CASCADE, Q
from django.db.models.signals import m2m_changed
from django.urls import reverse 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 django.contrib.contenttypes.fields import GenericRelation
from django.dispatch import receiver
from jsonfield import JSONField from jsonfield import JSONField
from lupa import LuaRuntime from lupa import LuaRuntime
from moss import ( from moss import (
@ -25,6 +28,7 @@ from judge.ratings import rate_contest
from judge.models.pagevote import PageVotable from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable from judge.models.bookmark import Bookmarkable
from judge.fulltext import SearchManager from judge.fulltext import SearchManager
from judge.caching import cache_wrapper
__all__ = [ __all__ = [
"Contest", "Contest",
@ -35,6 +39,9 @@ __all__ = [
"Rating", "Rating",
"ContestProblemClarification", "ContestProblemClarification",
"ContestsSummary", "ContestsSummary",
"OfficialContest",
"OfficialContestCategory",
"OfficialContestLocation",
] ]
@ -310,6 +317,15 @@ class Contest(models.Model, PageVotable, Bookmarkable):
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."),
) )
rate_limit = models.PositiveIntegerField(
verbose_name=(_("rate limit")),
null=True,
blank=True,
validators=[MinValueValidator(1), MaxValueValidator(5)],
help_text=_(
"Maximum number of submissions per minute. Leave empty if you don't want rate limit."
),
)
comments = GenericRelation("Comment") comments = GenericRelation("Comment")
pagevote = GenericRelation("PageVote") pagevote = GenericRelation("PageVote")
bookmark = GenericRelation("BookMark") bookmark = GenericRelation("BookMark")
@ -446,28 +462,44 @@ class Contest(models.Model, PageVotable, Bookmarkable):
def ended(self): def ended(self):
return self.end_time < self._now return self.end_time < self._now
@cached_property @cache_wrapper(prefix="Coai")
def author_ids(self): def _author_ids(self):
return Contest.authors.through.objects.filter(contest=self).values_list( return set(
"profile_id", flat=True Contest.authors.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
) )
@cached_property @cache_wrapper(prefix="Coci")
def editor_ids(self): def _curator_ids(self):
return self.author_ids.union( return set(
Contest.curators.through.objects.filter(contest=self).values_list( Contest.curators.through.objects.filter(contest=self).values_list(
"profile_id", flat=True "profile_id", flat=True
) )
) )
@cached_property @cache_wrapper(prefix="Coti")
def tester_ids(self): def _tester_ids(self):
return Contest.testers.through.objects.filter(contest=self).values_list( return set(
"profile_id", flat=True Contest.testers.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
) )
@cached_property
def author_ids(self):
return self._author_ids()
@cached_property
def editor_ids(self):
return self.author_ids.union(self._curator_ids())
@cached_property
def tester_ids(self):
return self._tester_ids()
def __str__(self): def __str__(self):
return self.name return f"{self.name} ({self.key})"
def get_absolute_url(self): def get_absolute_url(self):
return reverse("contest_view", args=(self.key,)) return reverse("contest_view", args=(self.key,))
@ -632,6 +664,20 @@ class Contest(models.Model, PageVotable, Bookmarkable):
verbose_name_plural = _("contests") verbose_name_plural = _("contests")
@receiver(m2m_changed, sender=Contest.organizations.through)
def update_organization_private(sender, instance, **kwargs):
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
instance.is_organization_private = instance.organizations.exists()
instance.save(update_fields=["is_organization_private"])
@receiver(m2m_changed, sender=Contest.private_contestants.through)
def update_private(sender, instance, **kwargs):
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
instance.is_private = instance.private_contestants.exists()
instance.save(update_fields=["is_private"])
class ContestParticipation(models.Model): class ContestParticipation(models.Model):
LIVE = 0 LIVE = 0
SPECTATE = -1 SPECTATE = -1
@ -920,6 +966,7 @@ class ContestsSummary(models.Model):
max_length=20, max_length=20,
unique=True, unique=True,
) )
results = models.JSONField(null=True, blank=True)
class Meta: class Meta:
verbose_name = _("contests summary") verbose_name = _("contests summary")
@ -930,3 +977,53 @@ class ContestsSummary(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("contests_summary", args=[self.key]) return reverse("contests_summary", args=[self.key])
class OfficialContestCategory(models.Model):
name = models.CharField(
max_length=50, verbose_name=_("official contest category"), unique=True
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("official contest category")
verbose_name_plural = _("official contest categories")
class OfficialContestLocation(models.Model):
name = models.CharField(
max_length=50, verbose_name=_("official contest location"), unique=True
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("official contest location")
verbose_name_plural = _("official contest locations")
class OfficialContest(models.Model):
contest = models.OneToOneField(
Contest,
verbose_name=_("contest"),
related_name="official",
on_delete=CASCADE,
)
category = models.ForeignKey(
OfficialContestCategory,
verbose_name=_("contest category"),
on_delete=CASCADE,
)
year = models.PositiveIntegerField(verbose_name=_("year"))
location = models.ForeignKey(
OfficialContestLocation,
verbose_name=_("contest location"),
on_delete=CASCADE,
)
class Meta:
verbose_name = _("official contest")
verbose_name_plural = _("official contests")

View file

@ -1,18 +1,20 @@
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from django.urls import reverse
from django.db.models import Q
from judge.models import Contest from judge.models import BlogPost, Problem
from judge.models.profile import Organization, Profile from judge.models.profile import Organization, Profile
__all__ = [
"Course",
"CourseRole",
"CourseResource",
"CourseAssignment",
]
course_directory_file = "" class RoleInCourse(models.TextChoices):
STUDENT = "ST", _("Student")
ASSISTANT = "AS", _("Assistant")
TEACHER = "TE", _("Teacher")
EDITABLE_ROLES = (RoleInCourse.TEACHER, RoleInCourse.ASSISTANT)
class Course(models.Model): class Course(models.Model):
@ -20,10 +22,7 @@ class Course(models.Model):
max_length=128, max_length=128,
verbose_name=_("course name"), verbose_name=_("course name"),
) )
about = models.TextField(verbose_name=_("organization description")) about = models.TextField(verbose_name=_("course description"))
ending_time = models.DateTimeField(
verbose_name=_("ending time"),
)
is_public = models.BooleanField( is_public = models.BooleanField(
verbose_name=_("publicly visible"), verbose_name=_("publicly visible"),
default=False, default=False,
@ -57,35 +56,50 @@ class Course(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@classmethod def get_absolute_url(self):
def is_editable_by(course, profile): return reverse("course_detail", args=(self.slug,))
if profile.is_superuser:
return True
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 @classmethod
def is_accessible_by(cls, course, profile): def is_editable_by(cls, course, profile):
userqueryset = CourseRole.objects.filter(course=course, user=profile) try:
if userqueryset.exists(): course_role = CourseRole.objects.get(course=course, user=profile)
return True return course_role.role in EDITABLE_ROLES
else: except CourseRole.DoesNotExist:
return False return False
@classmethod @classmethod
def get_students(cls, course): def is_accessible_by(cls, course, profile):
return CourseRole.objects.filter(course=course, role="ST").values("user") if not profile:
return False
try:
course_role = CourseRole.objects.get(course=course, user=profile)
if course_role.course.is_public:
return True
return course_role.role in EDITABLE_ROLES
except CourseRole.DoesNotExist:
return False
@classmethod @classmethod
def get_assistants(cls, course): def get_accessible_courses(cls, profile):
return CourseRole.objects.filter(course=course, role="AS").values("user") return Course.objects.filter(
Q(is_public=True) | Q(courserole__role__in=EDITABLE_ROLES),
courserole__user=profile,
).distinct()
@classmethod def _get_users_by_role(self, role):
def get_teachers(cls, course): course_roles = CourseRole.objects.filter(course=self, role=role).select_related(
return CourseRole.objects.filter(course=course, role="TE").values("user") "user"
)
return [course_role.user for course_role in course_roles]
def get_students(self):
return self._get_users_by_role(RoleInCourse.STUDENT)
def get_assistants(self):
return self._get_users_by_role(RoleInCourse.ASSISTANT)
def get_teachers(self):
return self._get_users_by_role(RoleInCourse.TEACHER)
@classmethod @classmethod
def add_student(cls, course, profiles): def add_student(cls, course, profiles):
@ -104,7 +118,7 @@ class Course(models.Model):
class CourseRole(models.Model): class CourseRole(models.Model):
course = models.OneToOneField( course = models.ForeignKey(
Course, Course,
verbose_name=_("course"), verbose_name=_("course"),
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -114,14 +128,9 @@ class CourseRole(models.Model):
Profile, Profile,
verbose_name=_("user"), verbose_name=_("user"),
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name=_("user_of_course"), related_name="course_roles",
) )
class RoleInCourse(models.TextChoices):
STUDENT = "ST", _("Student")
ASSISTANT = "AS", _("Assistant")
TEACHER = "TE", _("Teacher")
role = models.CharField( role = models.CharField(
max_length=2, max_length=2,
choices=RoleInCourse.choices, choices=RoleInCourse.choices,
@ -140,44 +149,19 @@ class CourseRole(models.Model):
couresrole.role = role couresrole.role = role
couresrole.save() couresrole.save()
class Meta:
unique_together = ("course", "user")
class CourseResource(models.Model):
course = models.OneToOneField( class CourseLesson(models.Model):
course = models.ForeignKey(
Course, Course,
verbose_name=_("course"), verbose_name=_("course"),
on_delete=models.CASCADE, related_name="lessons",
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,
)
class CourseAssignment(models.Model):
course = models.OneToOneField(
Course,
verbose_name=_("course"),
on_delete=models.CASCADE,
db_index=True,
)
contest = models.OneToOneField(
Contest,
verbose_name=_("contest"),
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
points = models.FloatField( title = models.TextField(verbose_name=_("course title"))
verbose_name=_("points"), content = models.TextField(verbose_name=_("course content"))
) problems = models.ManyToManyField(Problem, verbose_name=_("problem"), blank=True)
order = models.IntegerField(verbose_name=_("order"), default=0)
points = models.IntegerField(verbose_name=_("points"))

View file

@ -13,6 +13,7 @@ from mptt.models import MPTTModel
from judge.models.profile import Organization, Profile from judge.models.profile import Organization, Profile
from judge.models.pagevote import PageVotable from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable from judge.models.bookmark import Bookmarkable
from judge.caching import cache_wrapper
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"] __all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
@ -105,7 +106,7 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("blog_post", args=(self.id, self.slug)) return reverse("blog_post", args=(self.id, self.slug))
def can_see(self, user): def is_accessible_by(self, user):
if self.visible and self.publish_on <= timezone.now(): if self.visible and self.publish_on <= timezone.now():
if not self.is_organization_private: if not self.is_organization_private:
return True return True
@ -132,6 +133,10 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
and self.authors.filter(id=user.profile.id).exists() and self.authors.filter(id=user.profile.id).exists()
) )
@cache_wrapper(prefix="BPga", expected_type=models.query.QuerySet)
def get_authors(self):
return self.authors.only("id")
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

@ -31,11 +31,8 @@ class PageVote(models.Model):
@cache_wrapper(prefix="PVvs") @cache_wrapper(prefix="PVvs")
def vote_score(self, user): def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user) page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user).first()
if page_vote.exists(): return page_vote.score if page_vote else 0
return page_vote.first().score
else:
return 0
def __str__(self): def __str__(self):
return f"pagevote for {self.linked_object}" return f"pagevote for {self.linked_object}"
@ -52,11 +49,22 @@ class PageVoteVoter(models.Model):
verbose_name_plural = _("pagevote votes") verbose_name_plural = _("pagevote votes")
@cache_wrapper(prefix="gocp", expected_type=PageVote)
def _get_or_create_pagevote(content_type, object_id):
pagevote, created = PageVote.objects.get_or_create(
content_type=content_type,
object_id=object_id,
)
return pagevote
class PageVotable: class PageVotable:
def get_or_create_pagevote(self): def get_or_create_pagevote(self):
if self.pagevote.count(): content_type = ContentType.objects.get_for_model(self)
return self.pagevote.first() object_id = self.pk
new_pagevote = PageVote() return _get_or_create_pagevote(content_type, object_id)
new_pagevote.linked_object = self
new_pagevote.save()
return new_pagevote def dirty_pagevote(pagevote, profile):
pagevote.vote_score.dirty(pagevote, profile)
_get_or_create_pagevote.dirty(pagevote.content_type, pagevote.object_id)

View file

@ -11,6 +11,8 @@ 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 django.db.models.signals import m2m_changed
from django.dispatch import receiver
from judge.fulltext import SearchQuerySet from judge.fulltext import SearchQuerySet
from judge.models.pagevote import PageVotable from judge.models.pagevote import PageVotable
@ -22,6 +24,7 @@ from judge.models.problem_data import (
problem_data_storage, problem_data_storage,
problem_directory_file_helper, problem_directory_file_helper,
) )
from judge.caching import cache_wrapper
__all__ = [ __all__ = [
"ProblemGroup", "ProblemGroup",
@ -437,6 +440,10 @@ class Problem(models.Model, PageVotable, Bookmarkable):
"profile_id", flat=True "profile_id", flat=True
) )
@cache_wrapper(prefix="Pga", expected_type=models.query.QuerySet)
def get_authors(self):
return self.authors.only("id")
@cached_property @cached_property
def editor_ids(self): def editor_ids(self):
return self.author_ids.union( return self.author_ids.union(
@ -554,21 +561,36 @@ class Problem(models.Model, PageVotable, Bookmarkable):
cache.set(key, result) cache.set(key, result)
return result return result
def save(self, *args, **kwargs): def handle_code_change(self):
has_data = hasattr(self, "data_files")
has_pdf = bool(self.pdf_description)
if not has_data and not has_pdf:
return
try:
problem_data_storage.rename(self.__original_code, self.code)
except OSError as e:
if e.errno != errno.ENOENT:
raise
if has_pdf:
self.pdf_description.name = problem_directory_file_helper(
self.code, self.pdf_description.name
)
super().save(update_fields=["pdf_description"])
if has_data:
self.data_files._update_code(self.__original_code, self.code)
def save(self, should_move_data=True, *args, **kwargs):
code_changed = self.__original_code and self.code != self.__original_code
super(Problem, self).save(*args, **kwargs) super(Problem, self).save(*args, **kwargs)
if self.__original_code and self.code != self.__original_code: if code_changed and should_move_data:
if hasattr(self, "data_files") or self.pdf_description: self.handle_code_change()
try:
problem_data_storage.rename(self.__original_code, self.code) def delete(self, *args, **kwargs):
except OSError as e: super().delete(*args, **kwargs)
if e.errno != errno.ENOENT: problem_data_storage.delete_directory(self.code)
raise
if self.pdf_description:
self.pdf_description.name = problem_directory_file_helper(
self.code, self.pdf_description.name
)
if hasattr(self, "data_files"):
self.data_files._update_code(self.__original_code, self.code)
save.alters_data = True save.alters_data = True
@ -682,6 +704,10 @@ class Solution(models.Model, PageVotable, Bookmarkable):
else: else:
return reverse("problem_editorial", args=[problem.code]) return reverse("problem_editorial", args=[problem.code])
@cache_wrapper(prefix="Sga", expected_type=models.query.QuerySet)
def get_authors(self):
return self.authors.only("id")
def __str__(self): def __str__(self):
return _("Editorial for %s") % self.problem.name return _("Editorial for %s") % self.problem.name
@ -719,3 +745,10 @@ class ProblemPointsVote(models.Model):
def __str__(self): def __str__(self):
return f"{self.voter}: {self.points} for {self.problem.code}" return f"{self.voter}: {self.points} for {self.problem.code}"
@receiver(m2m_changed, sender=Problem.organizations.through)
def update_organization_private(sender, instance, **kwargs):
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
instance.is_organization_private = instance.organizations.exists()
instance.save(update_fields=["is_organization_private"])

View file

@ -38,7 +38,7 @@ CHECKERS = (
("identical", _("Byte identical")), ("identical", _("Byte identical")),
("linecount", _("Line-by-line")), ("linecount", _("Line-by-line")),
("custom", _("Custom checker (PY)")), ("custom", _("Custom checker (PY)")),
("customval", _("Custom validator (CPP)")), ("customcpp", _("Custom checker (CPP)")),
("interact", _("Interactive")), ("interact", _("Interactive")),
("testlib", _("Testlib")), ("testlib", _("Testlib")),
) )
@ -90,8 +90,8 @@ class ProblemData(models.Model):
upload_to=problem_directory_file, upload_to=problem_directory_file,
validators=[FileExtensionValidator(allowed_extensions=["py"])], validators=[FileExtensionValidator(allowed_extensions=["py"])],
) )
custom_validator = models.FileField( custom_checker_cpp = models.FileField(
verbose_name=_("custom validator file"), verbose_name=_("custom cpp checker file"),
storage=problem_data_storage, storage=problem_data_storage,
null=True, null=True,
blank=True, blank=True,
@ -186,9 +186,9 @@ class ProblemData(models.Model):
self.custom_checker.name = problem_directory_file_helper( self.custom_checker.name = problem_directory_file_helper(
new, self.custom_checker.name new, self.custom_checker.name
) )
if self.custom_validator: if self.custom_checker_cpp:
self.custom_validator.name = problem_directory_file_helper( self.custom_checker_cpp.name = problem_directory_file_helper(
new, self.custom_validator.name new, self.custom_checker_cpp.name
) )
if self.interactive_judge: if self.interactive_judge:
self.interactive_judge.name = problem_directory_file_helper( self.interactive_judge.name = problem_directory_file_helper(

View file

@ -17,7 +17,7 @@ from django.db.models.signals import post_save, pre_save
from fernet_fields import EncryptedCharField from fernet_fields import EncryptedCharField
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.choices import ACE_THEMES, TIMEZONE
from judge.models.runtime import Language from judge.models.runtime import Language
from judge.ratings import rating_class from judge.ratings import rating_class
from judge.caching import cache_wrapper from judge.caching import cache_wrapper
@ -26,6 +26,15 @@ from judge.caching import cache_wrapper
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"] __all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
TSHIRT_SIZES = (
("S", "Small (S)"),
("M", "Medium (M)"),
("L", "Large (L)"),
("XL", "Extra Large (XL)"),
("XXL", "2 Extra Large (XXL)"),
)
class EncryptedNullCharField(EncryptedCharField): class EncryptedNullCharField(EncryptedCharField):
def get_prep_value(self, value): def get_prep_value(self, value):
if not value: if not value:
@ -55,7 +64,9 @@ class Organization(models.Model):
verbose_name=_("short name"), verbose_name=_("short name"),
help_text=_("Displayed beside user name during contests"), help_text=_("Displayed beside user name during contests"),
) )
about = models.TextField(verbose_name=_("organization description")) about = models.CharField(
max_length=10000, verbose_name=_("organization description")
)
registrant = models.ForeignKey( registrant = models.ForeignKey(
"Profile", "Profile",
verbose_name=_("registrant"), verbose_name=_("registrant"),
@ -139,6 +150,14 @@ class Organization(models.Model):
def get_submissions_url(self): def get_submissions_url(self):
return reverse("organization_submissions", args=(self.id, self.slug)) return reverse("organization_submissions", args=(self.id, self.slug))
@cache_wrapper("Oia")
def is_admin(self, profile):
return self.admins.filter(id=profile.id).exists()
@cache_wrapper("Oim")
def is_member(self, profile):
return profile in self
class Meta: class Meta:
ordering = ["name"] ordering = ["name"]
permissions = ( permissions = (
@ -154,7 +173,9 @@ class Profile(models.Model):
user = models.OneToOneField( user = models.OneToOneField(
User, verbose_name=_("user associated"), on_delete=models.CASCADE User, verbose_name=_("user associated"), on_delete=models.CASCADE
) )
about = models.TextField(verbose_name=_("self-description"), null=True, blank=True) about = models.CharField(
max_length=10000, verbose_name=_("self-description"), null=True, blank=True
)
timezone = models.CharField( timezone = models.CharField(
max_length=50, max_length=50,
verbose_name=_("location"), verbose_name=_("location"),
@ -201,19 +222,7 @@ class Profile(models.Model):
help_text=_("User will not be ranked."), help_text=_("User will not be ranked."),
default=False, default=False,
) )
is_banned_problem_voting = models.BooleanField(
verbose_name=_("banned from voting"),
help_text=_("User will not be able to vote on problems' point values."),
default=False,
)
rating = models.IntegerField(null=True, default=None, db_index=True) rating = models.IntegerField(null=True, default=None, db_index=True)
user_script = models.TextField(
verbose_name=_("user script"),
default="",
blank=True,
max_length=65536,
help_text=_("User-defined JavaScript for site customization."),
)
current_contest = models.OneToOneField( current_contest = models.OneToOneField(
"ContestParticipation", "ContestParticipation",
verbose_name=_("current contest"), verbose_name=_("current contest"),
@ -222,13 +231,6 @@ class Profile(models.Model):
related_name="+", related_name="+",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
math_engine = models.CharField(
verbose_name=_("math engine"),
choices=MATH_ENGINES_CHOICES,
max_length=4,
default=settings.MATHOID_DEFAULT_TYPE,
help_text=_("the rendering engine used to render math"),
)
is_totp_enabled = models.BooleanField( is_totp_enabled = models.BooleanField(
verbose_name=_("2FA enabled"), verbose_name=_("2FA enabled"),
default=False, default=False,
@ -260,23 +262,9 @@ class Profile(models.Model):
max_length=300, max_length=300,
) )
@cache_wrapper(prefix="Pgbi2")
def _get_basic_info(self):
profile_image_url = None
if self.profile_image:
profile_image_url = self.profile_image.url
return {
"first_name": self.user.first_name,
"last_name": self.user.last_name,
"email": self.user.email,
"username": self.user.username,
"mute": self.mute,
"profile_image_url": profile_image_url,
}
@cached_property @cached_property
def _cached_info(self): def _cached_info(self):
return self._get_basic_info() return _get_basic_info(self.id)
@cached_property @cached_property
def organization(self): def organization(self):
@ -290,11 +278,11 @@ class Profile(models.Model):
@cached_property @cached_property
def first_name(self): def first_name(self):
return self._cached_info["first_name"] return self._cached_info.get("first_name", "")
@cached_property @cached_property
def last_name(self): def last_name(self):
return self._cached_info["last_name"] return self._cached_info.get("last_name", "")
@cached_property @cached_property
def email(self): def email(self):
@ -304,9 +292,17 @@ class Profile(models.Model):
def is_muted(self): def is_muted(self):
return self._cached_info["mute"] return self._cached_info["mute"]
@cached_property
def cached_display_rank(self):
return self._cached_info.get("display_rank")
@cached_property
def cached_rating(self):
return self._cached_info.get("rating")
@cached_property @cached_property
def profile_image_url(self): def profile_image_url(self):
return self._cached_info["profile_image_url"] return self._cached_info.get("profile_image_url")
@cached_property @cached_property
def count_unseen_notifications(self): def count_unseen_notifications(self):
@ -398,7 +394,7 @@ class Profile(models.Model):
@cached_property @cached_property
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.cached_display_rank, self.cached_rating)
def get_friends(self): # list of ids, including you def get_friends(self): # list of ids, including you
friend_obj = self.following_users.prefetch_related("users") friend_obj = self.following_users.prefetch_related("users")
@ -412,13 +408,16 @@ class Profile(models.Model):
if not self.user.is_authenticated: if not self.user.is_authenticated:
return False return False
profile_id = self.id profile_id = self.id
return ( return org.is_admin(self) or self.user.is_superuser
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id @classmethod
or self.user.is_superuser def prefetch_profile_cache(self, profile_ids):
) _get_basic_info.prefetch_multi([(pid,) for pid in profile_ids])
class Meta: class Meta:
indexes = [
models.Index(fields=["is_unlisted", "performance_points"]),
]
permissions = ( permissions = (
("test_site", "Shows in-progress development stuff"), ("test_site", "Shows in-progress development stuff"),
("totp", "Edit TOTP settings"), ("totp", "Edit TOTP settings"),
@ -427,6 +426,36 @@ class Profile(models.Model):
verbose_name_plural = _("user profiles") verbose_name_plural = _("user profiles")
class ProfileInfo(models.Model):
profile = models.OneToOneField(
Profile,
verbose_name=_("profile associated"),
on_delete=models.CASCADE,
related_name="info",
)
tshirt_size = models.CharField(
max_length=5,
choices=TSHIRT_SIZES,
verbose_name=_("t-shirt size"),
null=True,
blank=True,
)
date_of_birth = models.DateField(
verbose_name=_("date of birth"),
null=True,
blank=True,
)
address = models.CharField(
max_length=255,
verbose_name=_("address"),
null=True,
blank=True,
)
def __str__(self):
return f"{self.profile.user.username}'s Info"
class OrganizationRequest(models.Model): class OrganizationRequest(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
Profile, Profile,
@ -468,11 +497,7 @@ class Friend(models.Model):
@classmethod @classmethod
def is_friend(self, current_user, new_friend): def is_friend(self, current_user, new_friend):
try: try:
return ( return current_user.following_users.filter(users=new_friend).exists()
current_user.following_users.get()
.users.filter(user=new_friend.user)
.exists()
)
except: except:
return False return False
@ -506,7 +531,7 @@ class Friend(models.Model):
class OrganizationProfile(models.Model): class OrganizationProfile(models.Model):
users = models.ForeignKey( profile = models.ForeignKey(
Profile, Profile,
verbose_name=_("user"), verbose_name=_("user"),
related_name="last_visit", related_name="last_visit",
@ -525,37 +550,66 @@ class OrganizationProfile(models.Model):
) )
@classmethod @classmethod
def remove_organization(self, users, organization): def remove_organization(self, profile, organization):
organizationprofile = self.objects.filter( organization_profile = self.objects.filter(
users=users, organization=organization profile=profile, organization=organization
) )
if organizationprofile.exists(): if organization_profile.exists():
organizationprofile.delete() organization_profile.delete()
@classmethod @classmethod
def add_organization(self, users, organization): def add_organization(self, profile, organization):
self.remove_organization(users, organization) self.remove_organization(profile, organization)
new_organization = OrganizationProfile(users=users, organization=organization) new_row = OrganizationProfile(profile=profile, organization=organization)
new_organization.save() new_row.save()
@classmethod @classmethod
def get_most_recent_organizations(self, users): def get_most_recent_organizations(cls, profile):
return self.objects.filter(users=users).order_by("-last_visit")[:5] queryset = cls.objects.filter(profile=profile).order_by("-last_visit")[:5]
queryset = queryset.select_related("organization").defer("organization__about")
organizations = [op.organization for op in queryset]
return organizations
@receiver([post_save], sender=User) @receiver([post_save], sender=User)
def on_user_save(sender, instance, **kwargs): def on_user_save(sender, instance, **kwargs):
try: try:
profile = instance.profile profile = instance.profile
profile._get_basic_info.dirty(profile) _get_basic_info.dirty(profile.id)
except: except:
pass pass
@receiver([pre_save], sender=Profile) @cache_wrapper(prefix="Pgbi3", expected_type=dict)
def on_profile_save(sender, instance, **kwargs): def _get_basic_info(profile_id):
if instance.id is None: profile = (
return Profile.objects.select_related("user")
prev = sender.objects.get(id=instance.id) .only(
if prev.mute != instance.mute or prev.profile_image != instance.profile_image: "id",
instance._get_basic_info.dirty(instance) "mute",
"profile_image",
"user__username",
"user__email",
"user__first_name",
"user__last_name",
"display_rank",
"rating",
)
.get(id=profile_id)
)
user = profile.user
res = {
"email": user.email,
"username": user.username,
"mute": profile.mute,
"first_name": user.first_name or None,
"last_name": user.last_name or None,
"profile_image_url": profile.profile_image.url
if profile.profile_image
else None,
"display_rank": profile.display_rank,
"rating": profile.rating,
}
res = {k: v for k, v in res.items() if v is not None}
return res

View file

@ -11,6 +11,7 @@ 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.judgeapi import disconnect_judge from judge.judgeapi import disconnect_judge
from judge.caching import cache_wrapper
__all__ = ["Language", "RuntimeVersion", "Judge"] __all__ = ["Language", "RuntimeVersion", "Judge"]
@ -147,14 +148,11 @@ class Language(models.Model):
@classmethod @classmethod
def get_default_language(cls): def get_default_language(cls):
try: return _get_default_language()
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
except Language.DoesNotExist:
return cls.get_python3()
@classmethod @classmethod
def get_default_language_pk(cls): def get_default_language_pk(cls):
return cls.get_default_language().pk return _get_default_language().pk
class Meta: class Meta:
ordering = ["key"] ordering = ["key"]
@ -162,6 +160,14 @@ class Language(models.Model):
verbose_name_plural = _("languages") verbose_name_plural = _("languages")
@cache_wrapper(prefix="gdl")
def _get_default_language():
try:
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
except Language.DoesNotExist:
return cls.get_python3()
class RuntimeVersion(models.Model): class RuntimeVersion(models.Model):
language = models.ForeignKey( language = models.ForeignKey(
Language, Language,

View file

@ -220,13 +220,7 @@ class Submission(models.Model):
def id_secret(self): def id_secret(self):
return self.get_id_secret(self.id) return self.get_id_secret(self.id)
def is_accessible_by(self, profile): def is_accessible_by(self, profile, check_contest=True):
from judge.utils.problems import (
user_completed_ids,
user_tester_ids,
user_editable_ids,
)
if not profile: if not profile:
return False return False
@ -236,15 +230,6 @@ class Submission(models.Model):
if profile.id == self.user_id: if profile.id == self.user_id:
return True return True
if problem_id in user_editable_ids(profile):
return True
if self.problem_id in user_completed_ids(profile):
if self.problem.is_public:
return True
if problem_id in user_tester_ids(profile):
return True
if user.has_perm("judge.change_submission"): if user.has_perm("judge.change_submission"):
return True return True
@ -254,10 +239,26 @@ class Submission(models.Model):
if self.problem.is_public and user.has_perm("judge.view_public_submission"): if self.problem.is_public and user.has_perm("judge.view_public_submission"):
return True return True
contest = self.contest_object if check_contest:
if contest and contest.is_editable_by(user): contest = self.contest_object
if contest and contest.is_editable_by(user):
return True
from judge.utils.problems import (
user_completed_ids,
user_tester_ids,
user_editable_ids,
)
if problem_id in user_editable_ids(profile):
return True return True
if self.problem_id in user_completed_ids(profile):
if self.problem.is_public:
return True
if problem_id in user_tester_ids(profile):
return True
return False return False
class Meta: class Meta:
@ -276,6 +277,7 @@ class Submission(models.Model):
indexes = [ indexes = [
models.Index(fields=["problem", "user", "-points"]), models.Index(fields=["problem", "user", "-points"]),
models.Index(fields=["contest_object", "problem", "user", "-points"]), models.Index(fields=["contest_object", "problem", "user", "-points"]),
models.Index(fields=["language", "result"]),
] ]

View file

@ -0,0 +1,26 @@
import os
from django.db import models
from dmoj import settings
from django.utils.translation import gettext_lazy as _
__all__ = [
"TestFormatterModel",
]
def test_formatter_path(test_formatter, filename):
tail = filename.split(".")[-1]
head = filename.split(".")[0]
if str(tail).lower() != "zip":
raise Exception("400: Only ZIP files are supported")
new_filename = f"{head}.{tail}"
return os.path.join(settings.DMOJ_TEST_FORMATTER_ROOT, new_filename)
class TestFormatterModel(models.Model):
file = models.FileField(
verbose_name=_("testcase file"),
null=True,
blank=True,
upload_to=test_formatter_path,
)

View file

@ -7,7 +7,6 @@ from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
BETA2 = 328.33**2 BETA2 = 328.33**2
RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
MEAN_INIT = 1400.0 MEAN_INIT = 1400.0
@ -146,6 +145,8 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p):
def rate_contest(contest): def rate_contest(contest):
from judge.models import Rating, Profile from judge.models import Rating, Profile
from judge.models.profile import _get_basic_info
from judge.utils.users import get_contest_ratings, get_rating_rank
rating_subquery = Rating.objects.filter(user=OuterRef("user")) rating_subquery = Rating.objects.filter(user=OuterRef("user"))
rating_sorted = rating_subquery.order_by("-contest__end_time") rating_sorted = rating_subquery.order_by("-contest__end_time")
@ -237,6 +238,10 @@ def rate_contest(contest):
) )
) )
_get_basic_info.dirty_multi([(uid,) for uid in user_ids])
get_contest_ratings.dirty_multi([(uid,) for uid in user_ids])
get_rating_rank.dirty_multi([(uid,) for uid in user_ids])
RATING_LEVELS = [ RATING_LEVELS = [
"Newbie", "Newbie",

View file

@ -8,13 +8,13 @@ from django.core.cache.utils import make_template_fragment_key
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
import judge
from judge.utils.problems import finished_submission from judge.utils.problems import finished_submission
from .models import ( from .models import (
BlogPost, BlogPost,
Comment, Comment,
Contest, Contest,
ContestSubmission, ContestSubmission,
EFFECTIVE_MATH_ENGINES,
Judge, Judge,
Language, Language,
License, License,
@ -23,6 +23,8 @@ from .models import (
Problem, Problem,
Profile, Profile,
Submission, Submission,
NavigationBar,
Solution,
) )
@ -46,21 +48,13 @@ def problem_update(sender, instance, **kwargs):
cache.delete_many( cache.delete_many(
[ [
make_template_fragment_key("submission_problem", (instance.id,)), make_template_fragment_key("submission_problem", (instance.id,)),
make_template_fragment_key("problem_feed", (instance.id,)),
"problem_tls:%s" % instance.id, "problem_tls:%s" % instance.id,
"problem_mls:%s" % instance.id, "problem_mls:%s" % instance.id,
] ]
) )
cache.delete_many( cache.delete_many(
[ [
make_template_fragment_key("problem_html", (instance.id, engine, lang)) make_template_fragment_key("problem_html", (instance.id, lang))
for lang, _ in settings.LANGUAGES
for engine in EFFECTIVE_MATH_ENGINES
]
)
cache.delete_many(
[
make_template_fragment_key("problem_authors", (instance.id, lang))
for lang, _ in settings.LANGUAGES for lang, _ in settings.LANGUAGES
] ]
) )
@ -70,6 +64,7 @@ def problem_update(sender, instance, **kwargs):
for lang, _ in settings.LANGUAGES for lang, _ in settings.LANGUAGES
] ]
) )
Problem.get_authors.dirty(instance)
for lang, _ in settings.LANGUAGES: for lang, _ in settings.LANGUAGES:
unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang))) unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang)))
@ -77,20 +72,21 @@ def problem_update(sender, instance, **kwargs):
@receiver(post_save, sender=Profile) @receiver(post_save, sender=Profile)
def profile_update(sender, instance, **kwargs): def profile_update(sender, instance, **kwargs):
judge.utils.users.get_points_rank.dirty(instance.id)
judge.utils.users.get_rating_rank.dirty(instance.id)
if hasattr(instance, "_updating_stats_only"): if hasattr(instance, "_updating_stats_only"):
return return
cache.delete_many( cache.delete_many(
[ [make_template_fragment_key("user_about", (instance.id,))]
make_template_fragment_key("user_about", (instance.id, engine))
for engine in EFFECTIVE_MATH_ENGINES
]
+ [ + [
make_template_fragment_key("org_member_count", (org_id,)) make_template_fragment_key("org_member_count", (org_id,))
for org_id in instance.organizations.values_list("id", flat=True) for org_id in instance.organizations.values_list("id", flat=True)
] ]
) )
judge.models.profile._get_basic_info.dirty(instance.id)
@receiver(post_save, sender=Contest) @receiver(post_save, sender=Contest)
def contest_update(sender, instance, **kwargs): def contest_update(sender, instance, **kwargs):
@ -99,10 +95,7 @@ def contest_update(sender, instance, **kwargs):
cache.delete_many( cache.delete_many(
["generated-meta-contest:%d" % instance.id] ["generated-meta-contest:%d" % instance.id]
+ [ + [make_template_fragment_key("contest_html", (instance.id,))]
make_template_fragment_key("contest_html", (instance.id, engine))
for engine in EFFECTIVE_MATH_ENGINES
]
) )
@ -130,19 +123,8 @@ def comment_update(sender, instance, **kwargs):
@receiver(post_save, sender=BlogPost) @receiver(post_save, sender=BlogPost)
def post_update(sender, instance, **kwargs): def post_update(sender, instance, **kwargs):
cache.delete_many( cache.delete(make_template_fragment_key("post_content", (instance.id,)))
[ BlogPost.get_authors.dirty(instance)
make_template_fragment_key("post_summary", (instance.id,)),
"blog_slug:%d" % instance.id,
"blog_feed:%d" % instance.id,
]
)
cache.delete_many(
[
make_template_fragment_key("post_content", (instance.id, engine))
for engine in EFFECTIVE_MATH_ENGINES
]
)
@receiver(post_delete, sender=Submission) @receiver(post_delete, sender=Submission)
@ -159,12 +141,9 @@ def contest_submission_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Organization) @receiver(post_save, sender=Organization)
def organization_update(sender, instance, **kwargs): def organization_update(sender, instance, **kwargs):
cache.delete_many( cache.delete_many([make_template_fragment_key("organization_html", (instance.id,))])
[ for admin in instance.admins.all():
make_template_fragment_key("organization_html", (instance.id, engine)) Organization.is_admin.dirty(instance, admin)
for engine in EFFECTIVE_MATH_ENGINES
]
)
_misc_config_i18n = [code for code, _ in settings.LANGUAGES] _misc_config_i18n = [code for code, _ in settings.LANGUAGES]
@ -187,3 +166,13 @@ def contest_submission_update(sender, instance, **kwargs):
Submission.objects.filter(id=instance.submission_id).update( Submission.objects.filter(id=instance.submission_id).update(
contest_object_id=instance.participation.contest_id contest_object_id=instance.participation.contest_id
) )
@receiver(post_save, sender=NavigationBar)
def navbar_update(sender, instance, **kwargs):
judge.template_context._nav_bar.dirty()
@receiver(post_save, sender=Solution)
def solution_update(sender, instance, **kwargs):
cache.delete(make_template_fragment_key("solution_content", (instance.id,)))

View file

@ -9,6 +9,7 @@ from django.db import transaction
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from requests import HTTPError from requests import HTTPError
from reversion import revisions from reversion import revisions
from social_core.backends.github import GithubOAuth2 from social_core.backends.github import GithubOAuth2
@ -65,13 +66,13 @@ class UsernameForm(forms.Form):
max_length=30, max_length=30,
label="Username", label="Username",
error_messages={ error_messages={
"invalid": "A username must contain letters, numbers, or underscores" "invalid": _("A username must contain letters, numbers, or underscores")
}, },
) )
def clean_username(self): def clean_username(self):
if User.objects.filter(username=self.cleaned_data["username"]).exists(): if User.objects.filter(username=self.cleaned_data["username"]).exists():
raise forms.ValidationError("Sorry, the username is taken.") raise forms.ValidationError(_("Sorry, the username is taken."))
return self.cleaned_data["username"] return self.cleaned_data["username"]
@ -89,7 +90,7 @@ def choose_username(backend, user, username=None, *args, **kwargs):
request, request,
"registration/username_select.html", "registration/username_select.html",
{ {
"title": "Choose a username", "title": _("Choose a username"),
"form": form, "form": form,
}, },
) )
@ -118,7 +119,7 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs):
backend.strategy.request, backend.strategy.request,
"registration/profile_creation.html", "registration/profile_creation.html",
{ {
"title": "Create your profile", "title": _("Create your profile"),
"form": form, "form": form,
}, },
) )

View file

@ -8,7 +8,7 @@ from judge.utils.celery import Progress
__all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem") __all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem")
def apply_submission_filter(queryset, id_range, languages, results, contest): def apply_submission_filter(queryset, id_range, languages, results, contests):
if id_range: if id_range:
start, end = id_range start, end = id_range
queryset = queryset.filter(id__gte=start, id__lte=end) queryset = queryset.filter(id__gte=start, id__lte=end)
@ -16,8 +16,8 @@ def apply_submission_filter(queryset, id_range, languages, results, contest):
queryset = queryset.filter(language_id__in=languages) queryset = queryset.filter(language_id__in=languages)
if results: if results:
queryset = queryset.filter(result__in=results) queryset = queryset.filter(result__in=results)
if contest: if contests:
queryset = queryset.filter(contest_object=contest) queryset = queryset.filter(contest_object__in=contests)
queryset = queryset.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS) queryset = queryset.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS)
return queryset return queryset

View file

@ -1,3 +1,4 @@
import re
from functools import partial from functools import partial
from django.conf import settings from django.conf import settings
@ -6,7 +7,10 @@ from django.contrib.sites.shortcuts import get_current_site
from django.core.cache import cache from django.core.cache import cache
from django.utils.functional import SimpleLazyObject, new_method_proxy from django.utils.functional import SimpleLazyObject, new_method_proxy
from mptt.querysets import TreeQuerySet
from .models import MiscConfig, NavigationBar, Profile from .models import MiscConfig, NavigationBar, Profile
from judge.caching import cache_wrapper
class FixedSimpleLazyObject(SimpleLazyObject): class FixedSimpleLazyObject(SimpleLazyObject):
@ -24,7 +28,6 @@ def get_resource(request):
scheme = "http" scheme = "http"
return { return {
"PYGMENT_THEME": settings.PYGMENT_THEME,
"INLINE_JQUERY": settings.INLINE_JQUERY, "INLINE_JQUERY": settings.INLINE_JQUERY,
"INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME, "INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME,
"JQUERY_JS": settings.JQUERY_JS, "JQUERY_JS": settings.JQUERY_JS,
@ -51,22 +54,28 @@ def comet_location(request):
return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll} return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll}
@cache_wrapper(prefix="nb", expected_type=TreeQuerySet)
def _nav_bar():
return NavigationBar.objects.all()
def __nav_tab(path): def __nav_tab(path):
result = list( nav_bar_list = list(_nav_bar())
NavigationBar.objects.extra(where=["%s REGEXP BINARY regex"], params=[path])[:1] nav_bar_dict = {nb.id: nb for nb in nav_bar_list}
) result = next((nb for nb in nav_bar_list if re.match(nb.regex, path)), None)
return ( if result:
result[0].get_ancestors(include_self=True).values_list("key", flat=True) while result.parent_id:
if result result = nav_bar_dict.get(result.parent_id)
else [] return result.key
) else:
return []
def general_info(request): def general_info(request):
path = request.get_full_path() path = request.get_full_path()
return { return {
"nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)), "nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)),
"nav_bar": NavigationBar.objects.all(), "nav_bar": _nav_bar(),
"LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path, "LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path,
"perms": PermWrapper(request.user), "perms": PermWrapper(request.user),
} }
@ -119,13 +128,3 @@ def site_name(request):
"SITE_LONG_NAME": settings.SITE_LONG_NAME, "SITE_LONG_NAME": settings.SITE_LONG_NAME,
"SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL, "SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL,
} }
def math_setting(request):
if request.user.is_authenticated:
engine = request.profile.math_engine
else:
engine = settings.MATHOID_DEFAULT_TYPE
if engine == "auto":
engine = "jax"
return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax"}

View file

@ -1,5 +1,6 @@
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from judge.models import Profile from judge.models import Profile
@ -15,11 +16,13 @@ class LogUserAccessMiddleware(object):
hasattr(request, "user") hasattr(request, "user")
and request.user.is_authenticated and request.user.is_authenticated
and not getattr(request, "no_profile_update", False) and not getattr(request, "no_profile_update", False)
and not cache.get(f"user_log_update_{request.user.id}")
): ):
updates = {"last_access": now()} updates = {"last_access": now()}
# Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it. # Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it.
if request.META.get(settings.META_REMOTE_ADDRESS_KEY): if request.META.get(settings.META_REMOTE_ADDRESS_KEY):
updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY) updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY)
Profile.objects.filter(user_id=request.user.pk).update(**updates) Profile.objects.filter(user_id=request.user.pk).update(**updates)
cache.set(f"user_log_update_{request.user.id}", True, 120)
return response return response

View file

@ -8,7 +8,6 @@ def render_email_message(request, contexts):
email_contexts = { email_contexts = {
"username": request.user.username, "username": request.user.username,
"domain": current_site.domain, "domain": current_site.domain,
"protocol": "https" if request.is_secure() else "http",
"site_name": settings.SITE_NAME, "site_name": settings.SITE_NAME,
"message": None, "message": None,
"title": None, "title": None,

View file

@ -116,7 +116,7 @@ def infinite_paginate(queryset, page, page_size, pad_pages, paginator=None):
class InfinitePaginationMixin: class InfinitePaginationMixin:
pad_pages = 4 pad_pages = 2
@property @property
def use_infinite_pagination(self): def use_infinite_pagination(self):

View file

@ -4,6 +4,7 @@ import os
import re import re
import yaml import yaml
import zipfile import zipfile
import shutil
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -48,6 +49,13 @@ class ProblemDataStorage(FileSystemStorage):
def rename(self, old, new): def rename(self, old, new):
return os.rename(self.path(old), self.path(new)) return os.rename(self.path(old), self.path(new))
def delete_directory(self, name):
directory_path = self.path(name)
try:
shutil.rmtree(directory_path)
except FileNotFoundError:
pass
class ProblemDataError(Exception): class ProblemDataError(Exception):
def __init__(self, message): def __init__(self, message):
@ -82,8 +90,8 @@ class ProblemDataCompiler(object):
) )
return custom_checker_path[1] return custom_checker_path[1]
if case.checker == "customval": if case.checker == "customcpp":
custom_checker_path = split_path_first(case.custom_validator.name) custom_checker_path = split_path_first(case.custom_checker_cpp.name)
if len(custom_checker_path) != 2: if len(custom_checker_path) != 2:
raise ProblemDataError( raise ProblemDataError(
_("How did you corrupt the custom checker path?") _("How did you corrupt the custom checker path?")
@ -98,7 +106,7 @@ class ProblemDataCompiler(object):
} }
if case.checker == "testlib": if case.checker == "testlib":
custom_checker_path = split_path_first(case.custom_validator.name) custom_checker_path = split_path_first(case.custom_checker_cpp.name)
if len(custom_checker_path) != 2: if len(custom_checker_path) != 2:
raise ProblemDataError( raise ProblemDataError(
_("How did you corrupt the custom checker path?") _("How did you corrupt the custom checker path?")

View file

@ -1,7 +1,8 @@
from collections import defaultdict from collections import defaultdict
from math import e from math import e
from datetime import datetime from datetime import datetime, timedelta
import random import random
from enum import Enum
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -9,6 +10,7 @@ from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When
from django.db.models.fields import FloatField from django.db.models.fields import FloatField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _, gettext_noop from django.utils.translation import gettext as _, gettext_noop
from django.http import Http404
from judge.models import Problem, Submission from judge.models import Problem, Submission
from judge.ml.collab_filter import CollabFilter from judge.ml.collab_filter import CollabFilter
@ -112,13 +114,21 @@ def _get_result_data(results):
# Using gettext_noop here since this will be tacked into the cache, so it must be language neutral. # Using gettext_noop here since this will be tacked into the cache, so it must be language neutral.
# The caller, SubmissionList.get_result_data will run ugettext on the name. # The caller, SubmissionList.get_result_data will run ugettext on the name.
{"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]}, {"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]},
{"code": "WA", "name": gettext_noop("Wrong"), "count": results["WA"]}, {
"code": "WA",
"name": gettext_noop("Wrong Answer"),
"count": results["WA"],
},
{ {
"code": "CE", "code": "CE",
"name": gettext_noop("Compile Error"), "name": gettext_noop("Compile Error"),
"count": results["CE"], "count": results["CE"],
}, },
{"code": "TLE", "name": gettext_noop("Timeout"), "count": results["TLE"]}, {
"code": "TLE",
"name": gettext_noop("Time Limit Exceeded"),
"count": results["TLE"],
},
{ {
"code": "ERR", "code": "ERR",
"name": gettext_noop("Error"), "name": gettext_noop("Error"),
@ -165,7 +175,7 @@ def editable_problems(user, profile=None):
return subquery return subquery
@cache_wrapper(prefix="hp", timeout=900) @cache_wrapper(prefix="hp", timeout=14400)
def hot_problems(duration, limit): def hot_problems(duration, limit):
qs = Problem.get_public_problems().filter( qs = Problem.get_public_problems().filter(
submission__date__gt=timezone.now() - duration submission__date__gt=timezone.now() - duration
@ -222,7 +232,7 @@ def hot_problems(duration, limit):
return qs return qs
@cache_wrapper(prefix="grp", timeout=26400) @cache_wrapper(prefix="grp", timeout=14400)
def get_related_problems(profile, problem, limit=8): def get_related_problems(profile, problem, limit=8):
if not profile or not settings.ML_OUTPUT_PATH: if not profile or not settings.ML_OUTPUT_PATH:
return None return None
@ -248,3 +258,72 @@ def finished_submission(sub):
keys += ["contest_complete:%d" % participation.id] keys += ["contest_complete:%d" % participation.id]
keys += ["contest_attempted:%d" % participation.id] keys += ["contest_attempted:%d" % participation.id]
cache.delete_many(keys) cache.delete_many(keys)
class RecommendationType(Enum):
HOT_PROBLEM = 1
CF_DOT = 2
CF_COSINE = 3
CF_TIME_DOT = 4
CF_TIME_COSINE = 5
# Return a list of list. Each inner list correspond to each type in types
def get_user_recommended_problems(
user_id,
problem_ids,
recommendation_types,
limits,
shuffle=False,
):
cf_model = CollabFilter("collab_filter")
cf_time_model = CollabFilter("collab_filter_time")
def get_problem_ids_from_type(rec_type, limit):
if type(rec_type) == int:
try:
rec_type = RecommendationType(rec_type)
except ValueError:
raise Http404()
if rec_type == RecommendationType.HOT_PROBLEM:
return [
problem.id
for problem in hot_problems(timedelta(days=7), limit)
if problem.id in set(problem_ids)
]
if rec_type == RecommendationType.CF_DOT:
return cf_model.user_recommendations(
user_id, problem_ids, cf_model.DOT, limit
)
if rec_type == RecommendationType.CF_COSINE:
return cf_model.user_recommendations(
user_id, problem_ids, cf_model.COSINE, limit
)
if rec_type == RecommendationType.CF_TIME_DOT:
return cf_time_model.user_recommendations(
user_id, problem_ids, cf_model.DOT, limit
)
if rec_type == RecommendationType.CF_TIME_COSINE:
return cf_time_model.user_recommendations(
user_id, problem_ids, cf_model.COSINE, limit
)
return []
all_problems = []
for rec_type, limit in zip(recommendation_types, limits):
all_problems += get_problem_ids_from_type(rec_type, limit)
if shuffle:
seed = datetime.now().strftime("%d%m%Y")
random.Random(seed).shuffle(all_problems)
# deduplicate problems
res = []
used_pid = set()
for obj in all_problems:
if type(obj) == tuple:
obj = obj[1]
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res

67
judge/utils/users.py Normal file
View file

@ -0,0 +1,67 @@
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.translation import gettext as _, gettext_lazy
from judge.caching import cache_wrapper
from judge.models import Profile, Rating, Submission, Friend, ProfileInfo
@cache_wrapper(prefix="grr")
def get_rating_rank(profile):
if profile.is_unlisted:
return None
rank = None
if profile.rating:
rank = (
Profile.objects.filter(
is_unlisted=False,
rating__gt=profile.rating,
).count()
+ 1
)
return rank
@cache_wrapper(prefix="gpr")
def get_points_rank(profile):
if profile.is_unlisted:
return None
return (
Profile.objects.filter(
is_unlisted=False,
performance_points__gt=profile.performance_points,
).count()
+ 1
)
@cache_wrapper(prefix="gcr")
def get_contest_ratings(profile):
return (
profile.ratings.order_by("-contest__end_time")
.select_related("contest")
.defer("contest__description")
)
def get_awards(profile):
ratings = get_contest_ratings(profile)
sorted_ratings = sorted(
ratings, key=lambda x: (x.rank, -x.contest.end_time.timestamp())
)
result = [
{
"label": rating.contest.name,
"ranking": rating.rank,
"link": reverse("contest_ranking", args=(rating.contest.key,))
+ "#!"
+ profile.username,
"date": date_format(rating.contest.end_time, _("M j, Y")),
}
for rating in sorted_ratings
if rating.rank <= 3
]
return result

View file

@ -6,7 +6,7 @@ from django.utils.functional import lazy
from django.utils.translation import ugettext as _ 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.views.comment import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView from judge.views.bookmark import BookMarkDetailView
from judge.models import ( from judge.models import (
@ -23,9 +23,9 @@ from judge.models import (
from judge.models.profile import Organization, OrganizationProfile from judge.models.profile import Organization, OrganizationProfile
from judge.utils.cachedict import CacheDict from judge.utils.cachedict import CacheDict
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
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.utils.users import get_rating_rank, get_points_rank, get_awards
from judge.views.feed import FeedView from judge.views.feed import FeedView
@ -70,12 +70,37 @@ class HomeFeedView(FeedView):
profile_queryset = Profile.objects profile_queryset = Profile.objects
if self.request.organization: if self.request.organization:
profile_queryset = self.request.organization.members profile_queryset = self.request.organization.members
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by( context["top_rated"] = (
"-rating" profile_queryset.filter(is_unlisted=False)
)[:10] .order_by("-rating")
context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by( .only("id", "rating")[:10]
"-performance_points" )
)[:10] context["top_scorer"] = (
profile_queryset.filter(is_unlisted=False)
.order_by("-performance_points")
.only("id", "performance_points")[:10]
)
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
if self.request.user.is_authenticated:
context["rating_rank"] = get_rating_rank(self.request.profile)
context["points_rank"] = get_points_rank(self.request.profile)
medals_list = get_awards(self.request.profile)
context["awards"] = {
"medals": medals_list,
"gold_count": 0,
"silver_count": 0,
"bronze_count": 0,
}
for medal in medals_list:
if medal["ranking"] == 1:
context["awards"]["gold_count"] += 1
elif medal["ranking"] == 2:
context["awards"]["silver_count"] += 1
elif medal["ranking"] == 3:
context["awards"]["bronze_count"] += 1
return context return context
@ -91,7 +116,7 @@ class PostList(HomeFeedView):
queryset = ( queryset = (
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
.order_by("-sticky", "-publish_on") .order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations") .prefetch_related("organizations")
) )
filter = Q(is_organization_private=False) filter = Q(is_organization_private=False)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@ -126,7 +151,6 @@ class TicketFeed(HomeFeedView):
) )
.order_by("-id") .order_by("-id")
.prefetch_related("linked_item") .prefetch_related("linked_item")
.select_related("user__user")
) )
else: else:
return [] return []
@ -137,7 +161,6 @@ class TicketFeed(HomeFeedView):
Ticket.objects.order_by("-id") Ticket.objects.order_by("-id")
.filter(is_open=True) .filter(is_open=True)
.prefetch_related("linked_item") .prefetch_related("linked_item")
.select_related("user__user")
) )
return filter_visible_tickets(tickets, self.request.user, profile) return filter_visible_tickets(tickets, self.request.user, profile)
else: else:
@ -180,25 +203,24 @@ class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDeta
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PostView, self).get_context_data(**kwargs) context = super(PostView, self).get_context_data(**kwargs)
context["og_image"] = self.object.og_image context["og_image"] = self.object.og_image
context["valid_user_to_show_edit"] = False context["editable_orgs"] = []
context["valid_org_to_show_edit"] = []
if self.request.profile in self.object.authors.all(): orgs = list(self.object.organizations.all())
context["valid_user_to_show_edit"] = True
for valid_org_to_show_edit in self.object.organizations.all(): if self.request.profile:
if self.request.profile in valid_org_to_show_edit.admins.all(): if self.request.profile.id in self.object.get_authors():
context["valid_user_to_show_edit"] = True for org in orgs:
if org.is_member(self.request.profile):
if context["valid_user_to_show_edit"]: context["editable_orgs"].append(org)
for post_org in self.object.organizations.all(): else:
if post_org in self.request.profile.organizations.all(): for org in orgs:
context["valid_org_to_show_edit"].append(post_org) if org.is_admin(self.request.profile):
context["editable_orgs"].append(org)
return context return context
def get_object(self, queryset=None): def get_object(self, queryset=None):
post = super(PostView, self).get_object(queryset) post = super(PostView, self).get_object(queryset)
if not post.can_see(self.request.user): if not post.is_accessible_by(self.request.user):
raise Http404() raise Http404()
return post return post

View file

@ -8,13 +8,12 @@ from django.http import (
HttpResponseForbidden, HttpResponseForbidden,
) )
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from judge.models.bookmark import BookMark, MakeBookMark
from django.views.generic.base import TemplateResponseMixin from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from judge.dblock import LockModel
from django.views.generic import View, ListView from django.views.generic import View, ListView
from judge.models.bookmark import BookMark, MakeBookMark, dirty_bookmark
__all__ = [ __all__ = [
"dobookmark_page", "dobookmark_page",
@ -33,30 +32,31 @@ def bookmark_page(request, delta):
try: try:
bookmark_id = int(request.POST["id"]) bookmark_id = int(request.POST["id"])
bookmark_page = BookMark.objects.filter(id=bookmark_id) bookmark = BookMark.objects.get(id=bookmark_id)
except ValueError: except ValueError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: except BookMark.DoesNotExist:
if not bookmark_page.exists(): raise Http404()
raise Http404()
if delta == 0: if delta == 0:
bookmarklist = MakeBookMark.objects.filter( bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile bookmark=bookmark, user=request.profile
) )
if not bookmarklist.exists(): if not bookmarklist.exists():
newbookmark = MakeBookMark( newbookmark = MakeBookMark(
bookmark=bookmark_page.first(), bookmark=bookmark,
user=request.profile, user=request.profile,
) )
newbookmark.save() newbookmark.save()
else: else:
bookmarklist = MakeBookMark.objects.filter( bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile bookmark=bookmark, user=request.profile
) )
if bookmarklist.exists(): if bookmarklist.exists():
bookmarklist.delete() bookmarklist.delete()
dirty_bookmark(bookmark, request.profile)
return HttpResponse("success", content_type="text/plain") return HttpResponse("success", content_type="text/plain")

View file

@ -1,35 +1,46 @@
from django.conf import settings import json
from django import forms
from django.conf import settings
from django.contrib.auth.context_processors import PermWrapper
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.context_processors import PermWrapper from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, transaction from django.db import IntegrityError
from django.db.models import Q, F, Count, FilteredRelation from django.db.models import Count, F, FilteredRelation, Q
from django.db.models.expressions import Value
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.db.models.expressions import F, Value from django.forms import ModelForm
from django.forms.models import ModelForm
from django.http import ( from django.http import (
Http404, Http404,
HttpResponse, HttpResponse,
HttpResponseBadRequest, HttpResponseBadRequest,
HttpResponseForbidden, HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
) )
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.utils.datastructures import MultiValueDictKeyError
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView, View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from django_ratelimit.decorators import ratelimit
from django.contrib.contenttypes.models import ContentType
from reversion import revisions from reversion import revisions
from reversion.models import Version from reversion.models import Revision, Version
from judge.dblock import LockModel from judge.jinja2.reference import get_user_from_text
from judge.models import Comment, CommentVote, Notification, BlogPost from judge.models import BlogPost, Comment, CommentVote, Notification
from judge.models.notification import make_notification
from judge.models.comment import get_visible_comment_count
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.comments import add_mention_notifications
import json
__all__ = [ __all__ = [
"upvote_comment", "upvote_comment",
@ -39,7 +50,20 @@ __all__ = [
"CommentEdit", "CommentEdit",
] ]
DEFAULT_OFFSET = 10
def _get_html_link_notification(comment):
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
def add_mention_notifications(comment):
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
link = _get_html_link_notification(comment)
make_notification(users_mentioned, "Mention", link, comment.author)
@ratelimit(key="user", rate=settings.RL_VOTE)
@login_required @login_required
def vote_comment(request, delta): def vote_comment(request, delta):
if abs(delta) != 1: if abs(delta) != 1:
@ -77,27 +101,21 @@ def vote_comment(request, delta):
vote.voter = request.profile vote.voter = request.profile
vote.score = delta vote.score = delta
while True: try:
vote.save()
except IntegrityError:
try: try:
vote.save() vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile)
except IntegrityError: except CommentVote.DoesNotExist:
with LockModel(write=(CommentVote,)): raise Http404()
try: if -vote.score != delta:
vote = CommentVote.objects.get( return HttpResponseBadRequest(
comment_id=comment_id, voter=request.profile _("You already voted."), content_type="text/plain"
) )
except CommentVote.DoesNotExist: vote.delete()
# We must continue racing in case this is exploited to manipulate votes. Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
continue else:
if -vote.score != delta: Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
return HttpResponseBadRequest(
_("You already voted."), content_type="text/plain"
)
vote.delete()
Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
else:
Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
break
return HttpResponse("success", content_type="text/plain") return HttpResponse("success", content_type="text/plain")
@ -113,7 +131,7 @@ def get_comments(request, limit=10):
try: try:
comment_id = int(request.GET["id"]) comment_id = int(request.GET["id"])
parent_none = int(request.GET["parent_none"]) parent_none = int(request.GET["parent_none"])
except ValueError: except (ValueError, MultiValueDictKeyError):
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:
if comment_id and not Comment.objects.filter(id=comment_id).exists(): if comment_id and not Comment.objects.filter(id=comment_id).exists():
@ -121,7 +139,10 @@ def get_comments(request, limit=10):
offset = 0 offset = 0
if "offset" in request.GET: if "offset" in request.GET:
offset = int(request.GET["offset"]) try:
offset = int(request.GET["offset"])
except ValueError:
return HttpResponseBadRequest()
target_comment = -1 target_comment = -1
if "target_comment" in request.GET: if "target_comment" in request.GET:
@ -147,7 +168,6 @@ def get_comments(request, limit=10):
.defer("author__about") .defer("author__about")
.annotate( .annotate(
count_replies=Count("replies", distinct=True), count_replies=Count("replies", distinct=True),
revisions=Count("versions", distinct=True),
)[offset : offset + limit] )[offset : offset + limit]
) )
profile = None profile = None
@ -241,8 +261,9 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
# update notifications # update notifications
comment = form.instance comment = form.instance
add_mention_notifications(comment) add_mention_notifications(comment)
comment.revision_count = comment.versions.count() + 1
with transaction.atomic(), revisions.create_revision(): comment.save(update_fields=["revision_count"])
with revisions.create_revision():
revisions.set_comment(_("Edited from site")) revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
return super(CommentEditAjax, self).form_valid(form) return super(CommentEditAjax, self).form_valid(form)
@ -294,4 +315,195 @@ def comment_hide(request):
comment = get_object_or_404(Comment, id=comment_id) comment = get_object_or_404(Comment, id=comment_id)
comment.get_descendants(include_self=True).update(hidden=True) comment.get_descendants(include_self=True).update(hidden=True)
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
return HttpResponse("ok") return HttpResponse("ok")
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ["body", "parent"]
widgets = {
"parent": forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets["body"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("comment_preview"),
preview_timeout=1000,
hide_preview_button=True,
)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_("Your part is silent, little toad."))
elif (
not self.request.user.is_staff
and not profile.submission_set.filter(
points=F("problem__points")
).exists()
):
raise ValidationError(
_(
"You need to have solved at least one problem "
"before your voice can be heard."
)
)
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"):
return False
return (
self.request.in_contest
and self.request.participation.contest.use_clarifications
)
@method_decorator(ratelimit(key="user", rate=settings.RL_COMMENT))
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get("parent")
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not self.object.comments.filter(hidden=False, id=parent).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.linked_object = self.object
with revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_("Posted comment"))
comment.save()
# add notification for reply
comment_notif_link = _get_html_link_notification(comment)
if comment.parent and comment.parent.author != comment.author:
make_notification(
[comment.parent.author], "Reply", comment_notif_link, comment.author
)
# add notification for page authors
page_authors = comment.linked_object.authors.all()
make_notification(
page_authors, "Comment", comment_notif_link, comment.author
)
add_mention_notifications(comment)
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
return HttpResponseRedirect(comment.get_absolute_url())
context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context)
def get(self, request, *args, **kwargs):
target_comment = None
self.object = self.get_object()
if "comment-id" in request.GET:
try:
comment_id = int(request.GET["comment-id"])
comment_obj = Comment.objects.get(id=comment_id)
except (Comment.DoesNotExist, ValueError):
raise Http404
if comment_obj.linked_object != self.object:
raise Http404
target_comment = comment_obj.get_root()
return self.render_to_response(
self.get_context_data(
object=self.object,
target_comment=target_comment,
comment_form=CommentForm(request, initial={"parent": None}),
)
)
def _get_queryset(self, target_comment):
if target_comment:
queryset = target_comment.get_descendants(include_self=True)
queryset = queryset.filter(hidden=False)
else:
queryset = self.object.comments
queryset = queryset.filter(parent=None, hidden=False)
queryset = queryset.filter(hidden=False).annotate(
count_replies=Count("replies", distinct=True),
)[:DEFAULT_OFFSET]
if self.request.user.is_authenticated:
profile = self.request.profile
queryset = queryset.annotate(
my_vote=FilteredRelation(
"votes", condition=Q(votes__voter_id=profile.id)
),
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
return queryset
def get_context_data(self, target_comment=None, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = self._get_queryset(target_comment)
comment_count = self.object.comments.filter(parent=None, hidden=False).count()
content_type = ContentType.objects.get_for_model(self.object)
all_comment_count = get_visible_comment_count(content_type, self.object.pk)
if target_comment != None:
context["target_comment"] = target_comment.id
else:
context["target_comment"] = -1
if self.request.user.is_authenticated:
context["is_new_user"] = (
not self.request.user.is_staff
and not self.request.profile.submission_set.filter(
points=F("problem__points")
).exists()
)
context["comment_lock"] = self.is_comment_locked()
context["comment_list"] = list(queryset)
context["has_comments"] = len(context["comment_list"]) > 0
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
if queryset.exists():
context["comment_root_id"] = context["comment_list"][0].id
else:
context["comment_root_id"] = 0
context["comment_parent_none"] = 1
if target_comment != None:
context["offset"] = 0
context["comment_more"] = comment_count - 1
else:
context["offset"] = DEFAULT_OFFSET
context["comment_more"] = comment_count - DEFAULT_OFFSET
context["limit"] = DEFAULT_OFFSET
context["comment_count"] = comment_count
context["profile"] = self.request.profile
context["all_comment_count"] = all_comment_count
return context

View file

@ -27,7 +27,6 @@ from django.db.models import (
Value, Value,
When, When,
) )
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.expressions import CombinedExpression from django.db.models.expressions import CombinedExpression
from django.http import ( from django.http import (
@ -56,7 +55,7 @@ from django.views.generic.detail import (
) )
from judge import event_poster as event from judge import event_poster as event
from judge.comments import CommentedDetailView from judge.views.comment import CommentedDetailView
from judge.forms import ContestCloneForm from judge.forms import ContestCloneForm
from judge.models import ( from judge.models import (
Contest, Contest,
@ -70,6 +69,8 @@ from judge.models import (
Submission, Submission,
ContestProblemClarification, ContestProblemClarification,
ContestsSummary, ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
) )
from judge.tasks import run_moss from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status from judge.utils.celery import redirect_to_task_status
@ -107,6 +108,7 @@ __all__ = [
"base_contest_ranking_list", "base_contest_ranking_list",
"ContestClarificationView", "ContestClarificationView",
"update_contest_mode", "update_contest_mode",
"OfficialContestList",
] ]
@ -130,8 +132,17 @@ def _find_contest(request, key):
class ContestListMixin(object): class ContestListMixin(object):
official = False
def get_queryset(self): def get_queryset(self):
return Contest.get_visible_contests(self.request.user) q = Contest.get_visible_contests(self.request.user)
if self.official:
q = q.filter(official__isnull=False).select_related(
"official", "official__category", "official__location"
)
else:
q = q.filter(official__isnull=True)
return q
class ContestList( class ContestList(
@ -141,119 +152,190 @@ class ContestList(
paginate_by = 10 paginate_by = 10
template_name = "contest/list.html" template_name = "contest/list.html"
title = gettext_lazy("Contests") title = gettext_lazy("Contests")
context_object_name = "past_contests"
all_sorts = frozenset(("name", "user_count", "start_time")) all_sorts = frozenset(("name", "user_count", "start_time"))
default_desc = frozenset(("name", "user_count")) default_desc = frozenset(("name", "user_count"))
default_sort = "-start_time" context_object_name = "contests"
def get_default_sort_order(self, request):
if request.GET.get("contest") and settings.ENABLE_FTS:
return "-relevance"
if self.current_tab == "future":
return "start_time"
return "-start_time"
@cached_property @cached_property
def _now(self): def _now(self):
return timezone.now() return timezone.now()
def get(self, request, *args, **kwargs): def GET_with_session(self, request, key):
self.contest_query = None if not request.GET.get(key):
self.org_query = [] return request.session.get(key, False)
self.show_orgs = 0 return request.GET.get(key, None) == "1"
if request.GET.get("show_orgs"):
self.show_orgs = 1
if "orgs" in self.request.GET and self.request.profile: def setup_contest_list(self, request):
self.contest_query = request.GET.get("contest", "")
self.hide_organization_contests = 0
if self.GET_with_session(request, "hide_organization_contests"):
self.hide_organization_contests = 1
self.org_query = []
if request.GET.get("orgs") and 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: if not 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( in set(
"id", flat=True request.profile.organizations.values_list("id", flat=True)
) )
] ]
except ValueError: except ValueError:
pass pass
def get(self, request, *args, **kwargs):
default_tab = "active"
if not self.request.user.is_authenticated:
default_tab = "current"
self.current_tab = self.request.GET.get("tab", default_tab)
self.setup_contest_list(request)
return super(ContestList, self).get(request, *args, **kwargs) return super(ContestList, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
to_update = ("hide_organization_contests",)
for key in to_update:
if key in request.GET:
val = request.GET.get(key) == "1"
request.session[key] = val
else:
request.session[key] = False
return HttpResponseRedirect(request.get_full_path())
def extra_queryset_filters(self, queryset):
return queryset
def _get_queryset(self): def _get_queryset(self):
queryset = ( queryset = (
super(ContestList, self) super(ContestList, self)
.get_queryset() .get_queryset()
.prefetch_related("tags", "organizations", "authors", "curators", "testers") .prefetch_related("tags", "organizations")
) )
if "contest" in self.request.GET: if self.contest_query:
self.contest_query = query = " ".join( substr_queryset = queryset.filter(
self.request.GET.getlist("contest") Q(key__icontains=self.contest_query)
).strip() | Q(name__icontains=self.contest_query)
if query: )
substr_queryset = queryset.filter( if settings.ENABLE_FTS:
Q(key__icontains=query) | Q(name__icontains=query) queryset = (
queryset.search(self.contest_query).extra(order_by=["-relevance"])
| substr_queryset
) )
if settings.ENABLE_FTS: else:
queryset = ( queryset = substr_queryset
queryset.search(query).extra(order_by=["-relevance"])
| substr_queryset
)
else:
queryset = substr_queryset
if not self.org_query and self.request.organization: if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id] self.org_query = [self.request.organization.id]
if self.show_orgs: if self.hide_organization_contests:
queryset = queryset.filter(organizations=None) 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)
queryset = self.extra_queryset_filters(queryset)
return queryset return queryset
def get_queryset(self): def _get_past_contests_queryset(self):
return ( return (
self._get_queryset() self._get_queryset()
.order_by(self.order, "key")
.filter(end_time__lt=self._now) .filter(end_time__lt=self._now)
.order_by(self.order, "key")
) )
def _active_participations(self):
return ContestParticipation.objects.filter(
virtual=0,
user=self.request.profile,
contest__start_time__lte=self._now,
contest__end_time__gte=self._now,
)
@cached_property
def _active_contests_ids(self):
return [
participation.contest_id
for participation in self._active_participations().select_related("contest")
if not participation.ended
]
def _get_current_contests_queryset(self):
return (
self._get_queryset()
.exclude(id__in=self._active_contests_ids)
.filter(start_time__lte=self._now, end_time__gte=self._now)
.order_by(self.order, "key")
)
def _get_future_contests_queryset(self):
return (
self._get_queryset()
.filter(start_time__gt=self._now)
.order_by(self.order, "key")
)
def _get_active_participations_queryset(self):
active_contests = (
self._get_queryset()
.filter(id__in=self._active_contests_ids)
.order_by(self.order, "key")
)
ordered_ids = list(active_contests.values_list("id", flat=True))
participations = self._active_participations().filter(
contest_id__in=ordered_ids
)
participations = sorted(
participations, key=lambda p: ordered_ids.index(p.contest_id)
)
return participations
def get_queryset(self):
if self.current_tab == "past":
return self._get_past_contests_queryset()
elif self.current_tab == "current":
return self._get_current_contests_queryset()
elif self.current_tab == "future":
return self._get_future_contests_queryset()
else: # Default to active
return self._get_active_participations_queryset()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ContestList, self).get_context_data(**kwargs) context = super(ContestList, self).get_context_data(**kwargs)
present, active, future = [], [], []
for contest in self._get_queryset().exclude(end_time__lt=self._now):
if contest.start_time > self._now:
future.append(contest)
else:
present.append(contest)
if self.request.user.is_authenticated: context["current_tab"] = self.current_tab
for participation in (
ContestParticipation.objects.filter( context["current_count"] = self._get_current_contests_queryset().count()
virtual=0, user=self.request.profile, contest_id__in=present context["future_count"] = self._get_future_contests_queryset().count()
) context["active_count"] = len(self._get_active_participations_queryset())
.select_related("contest")
.prefetch_related(
"contest__authors", "contest__curators", "contest__testers"
)
.annotate(key=F("contest__key"))
):
if not participation.ended:
active.append(participation)
present.remove(participation.contest)
if not ("contest" in self.request.GET and settings.ENABLE_FTS):
active.sort(key=attrgetter("end_time", "key"))
present.sort(key=attrgetter("end_time", "key"))
future.sort(key=attrgetter("start_time"))
context["active_participations"] = active
context["current_contests"] = present
context["future_contests"] = future
context["now"] = self._now context["now"] = self._now
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) context["hide_organization_contests"] = int(self.hide_organization_contests)
if self.request.profile: if self.request.profile:
if self.request.user.is_superuser: context["organizations"] = self.request.profile.organizations.all()
context["organizations"] = Organization.objects.all()
else:
context["organizations"] = self.request.profile.organizations.all()
context["page_type"] = "list" context["page_type"] = "list"
context["selected_order"] = self.request.GET.get("order")
context["all_sort_options"] = [
("start_time", _("Start time (asc.)")),
("-start_time", _("Start time (desc.)")),
("name", _("Name (asc.)")),
("-name", _("Name (desc.)")),
("user_count", _("User count (asc.)")),
("-user_count", _("User count (desc.)")),
]
context.update(self.get_sort_context()) context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context()) context.update(self.get_sort_paginate_context())
return context return context
@ -346,6 +428,19 @@ class ContestMixin(object):
return context return context
def contest_access_check(self, contest):
try:
contest.access_check(self.request.user)
except Contest.PrivateContest:
raise PrivateContestError(
contest.name,
contest.is_private,
contest.is_organization_private,
contest.organizations.all(),
)
except Contest.Inaccessible:
raise Http404()
def get_object(self, queryset=None): def get_object(self, queryset=None):
contest = super(ContestMixin, self).get_object(queryset) contest = super(ContestMixin, self).get_object(queryset)
profile = self.request.profile profile = self.request.profile
@ -361,19 +456,9 @@ class ContestMixin(object):
if self.should_bypass_access_check(contest): if self.should_bypass_access_check(contest):
return contest return contest
try: self.contest_access_check(contest)
contest.access_check(self.request.user)
except Contest.PrivateContest: return contest
raise PrivateContestError(
contest.name,
contest.is_private,
contest.is_organization_private,
contest.organizations.all(),
)
except Contest.Inaccessible:
raise Http404()
else:
return contest
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
@ -449,6 +534,10 @@ class ContestDetail(
) )
context["editable_organizations"] = self.get_editable_organizations() context["editable_organizations"] = self.get_editable_organizations()
context["is_clonable"] = is_contest_clonable(self.request, self.object) context["is_clonable"] = is_contest_clonable(self.request, self.object)
if self.request.in_contest:
context["current_contest"] = self.request.participation.contest
else:
context["current_contest"] = None
return context return context
@ -459,7 +548,12 @@ def is_contest_clonable(request, contest):
return False return False
if request.user.has_perm("judge.clone_contest"): if request.user.has_perm("judge.clone_contest"):
return True return True
if contest.ended: if contest.access_code and not contest.is_editable_by(request.user):
return False
if (
contest.end_time is not None
and contest.end_time + timedelta(days=1) < contest._now
):
return True return True
return False return False
@ -498,6 +592,7 @@ class ContestClone(ContestMixin, TitleMixin, SingleObjectFormView):
contest.is_visible = False contest.is_visible = False
contest.user_count = 0 contest.user_count = 0
contest.key = form.cleaned_data["key"] contest.key = form.cleaned_data["key"]
contest.is_rated = False
contest.save() contest.save()
contest.tags.set(tags) contest.tags.set(tags)
@ -562,12 +657,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
profile = request.profile profile = request.profile
if profile.current_contest is not None: if profile.current_contest is not None:
return generic_message( profile.remove_contest()
request,
_("Already in contest"),
_('You are already in a contest: "%s".')
% profile.current_contest.contest.name,
)
if ( if (
not request.user.is_superuser not request.user.is_superuser
@ -646,6 +736,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
profile.save() profile.save()
contest._updating_stats_only = True contest._updating_stats_only = True
contest.update_user_count() contest.update_user_count()
request.session["contest_mode"] = True
return HttpResponseRedirect(reverse("problem_list")) return HttpResponseRedirect(reverse("problem_list"))
def ask_for_access_code(self, form=None): def ask_for_access_code(self, form=None):
@ -882,7 +973,10 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
if (point == None) or (problem_code not in codes): if (point == None) or (problem_code not in codes):
continue continue
problem_idx = codes.index(problem_code) problem_idx = codes.index(problem_code)
bin_idx = math.floor(point * self.POINT_BIN / max_point) if max_point > 0:
bin_idx = math.floor(point * self.POINT_BIN / max_point)
else:
bin_idx = 0
bin_idx = max(min(bin_idx, self.POINT_BIN), 0) bin_idx = max(min(bin_idx, self.POINT_BIN), 0)
counter[problem_idx][bin_idx] += count counter[problem_idx][bin_idx] += count
for i in range(num_problems): for i in range(num_problems):
@ -936,7 +1030,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
ContestRankingProfile = namedtuple( ContestRankingProfile = namedtuple(
"ContestRankingProfile", "ContestRankingProfile",
"id user css_class username points cumtime tiebreaker organization participation " "id user username points cumtime tiebreaker participation "
"participation_rating problem_cells result_cell", "participation_rating problem_cells result_cell",
) )
@ -956,13 +1050,11 @@ def make_contest_ranking_profile(
user = participation.user user = participation.user
return ContestRankingProfile( return ContestRankingProfile(
id=user.id, id=user.id,
user=user.user, user=user,
css_class=user.css_class,
username=user.username, username=user.username,
points=points, points=points,
cumtime=cumtime, cumtime=cumtime,
tiebreaker=participation.tiebreaker, tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating participation_rating=participation.rating.rating
if hasattr(participation, "rating") if hasattr(participation, "rating")
else None, else None,
@ -979,45 +1071,60 @@ def make_contest_ranking_profile(
) )
def base_contest_ranking_list(contest, problems, queryset, show_final=False): def base_contest_ranking_list(
return [ contest, problems, queryset, show_final=False, extra_participation=None
make_contest_ranking_profile(contest, participation, problems, show_final) ):
for participation in queryset.select_related("user__user", "rating").defer( participation_fields = [
"user__about", "user__organizations__about" field.name
) for field in ContestParticipation._meta.get_fields()
if field.concrete and not field.many_to_many
]
fields_to_fetch = participation_fields + [
"user__id",
"rating__rating",
] ]
res = [
make_contest_ranking_profile(contest, participation, problems, show_final)
for participation in queryset.select_related("user", "rating").only(
*fields_to_fetch
)
]
Profile.prefetch_profile_cache([p.id for p in res])
return res
def contest_ranking_list(contest, problems, queryset=None, show_final=False):
if not queryset: def contest_ranking_list(
contest, problems, queryset=None, show_final=False, extra_participation=None
):
if queryset is None:
queryset = contest.users.filter(virtual=0) queryset = contest.users.filter(virtual=0)
if not show_final: if extra_participation and extra_participation.virtual:
return base_contest_ranking_list( queryset = queryset | contest.users.filter(id=extra_participation.id)
contest,
problems, if show_final:
queryset.prefetch_related("user__organizations") queryset = queryset.order_by(
.extra(select={"round_score": "round(score, 6)"}) "is_disqualified", "-score_final", "cumtime_final", "tiebreaker"
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
show_final,
) )
else: else:
return base_contest_ranking_list( queryset = queryset.order_by(
contest, "is_disqualified", "-score", "cumtime", "tiebreaker"
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score_final, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"),
show_final,
) )
return base_contest_ranking_list(
contest,
problems,
queryset,
show_final,
)
def get_contest_ranking_list( def get_contest_ranking_list(
request, request,
contest, contest,
participation=None, participation=None,
ranking_list=contest_ranking_list, ranking_list=contest_ranking_list,
show_current_virtual=False,
ranker=ranker, ranker=ranker,
show_final=False, show_final=False,
): ):
@ -1027,21 +1134,17 @@ def get_contest_ranking_list(
.order_by("order") .order_by("order")
) )
users = ranker( if participation is None:
ranking_list(contest, problems, show_final=show_final), participation = _get_current_virtual_participation(request, contest)
key=attrgetter("points", "cumtime", "tiebreaker"),
ranking_list_result = ranking_list(
contest, problems, show_final=show_final, extra_participation=participation
) )
if show_current_virtual: users = ranker(
if participation is None and request.user.is_authenticated: ranking_list_result,
participation = request.profile.current_contest key=attrgetter("points", "cumtime", "tiebreaker"),
if participation is None or participation.contest_id != contest.id: )
participation = None
if participation is not None and participation.virtual:
users = chain(
[("-", make_contest_ranking_profile(contest, participation, problems))],
users,
)
return users, problems return users, problems
@ -1061,6 +1164,9 @@ def contest_ranking_ajax(request, contest, participation=None):
): ):
raise Http404() raise Http404()
if participation is None:
participation = _get_current_virtual_participation(request, contest)
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 = request.profile.get_friends() friends = request.profile.get_friends()
@ -1072,7 +1178,9 @@ def contest_ranking_ajax(request, contest, participation=None):
request, request,
contest, contest,
participation, participation,
ranking_list=partial(contest_ranking_list, queryset=queryset), ranking_list=partial(
contest_ranking_list, queryset=queryset, extra_participation=participation
),
show_final=show_final, show_final=show_final,
) )
return render( return render(
@ -1088,6 +1196,19 @@ def contest_ranking_ajax(request, contest, participation=None):
) )
def _get_current_virtual_participation(request, contest):
# Return None if not eligible
if not request.user.is_authenticated:
return None
participation = request.profile.current_contest
if participation is None or participation.contest_id != contest.id:
return None
return participation
class ContestRankingBase(ContestMixin, TitleMixin, DetailView): class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
template_name = "contest/ranking.html" template_name = "contest/ranking.html"
page_type = None page_type = None
@ -1182,7 +1303,6 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
return get_contest_ranking_list( return get_contest_ranking_list(
self.request, self.request,
self.object, self.object,
show_current_virtual=False,
ranking_list=partial(base_contest_ranking_list, queryset=queryset), ranking_list=partial(base_contest_ranking_list, queryset=queryset),
ranker=lambda users, key: ( ranker=lambda users, key: (
(user.participation.virtual or live_link, user) for user in users (user.participation.virtual or live_link, user) for user in users
@ -1418,30 +1538,43 @@ def update_contest_mode(request):
ContestsSummaryData = namedtuple( ContestsSummaryData = namedtuple(
"ContestsSummaryData", "ContestsSummaryData",
"user points point_contests css_class", "username first_name last_name points point_contests css_class",
) )
def contests_summary_view(request, key): class ContestsSummaryView(DiggPaginatorMixin, ListView):
try: paginate_by = 50
contests_summary = ContestsSummary.objects.get(key=key) template_name = "contest/contests_summary.html"
except:
raise Http404()
cache_key = "csv:" + key def get(self, *args, **kwargs):
context = cache.get(cache_key) try:
if context: self.contests_summary = ContestsSummary.objects.get(key=kwargs["key"])
return render(request, "contest/contests_summary.html", context) except:
raise Http404()
return super().get(*args, **kwargs)
scores_system = contests_summary.scores def get_queryset(self):
contests = contests_summary.contests.all() total_rank = self.contests_summary.results
return total_rank
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["contests"] = self.contests_summary.contests.all()
context["title"] = _("Contests")
context["first_page_href"] = "."
return context
def recalculate_contest_summary_result(contest_summary):
scores_system = contest_summary.scores
contests = contest_summary.contests.all()
total_points = defaultdict(int) total_points = defaultdict(int)
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests)) result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
user_css_class = {} user_css_class = {}
for i in range(len(contests)): for i in range(len(contests)):
contest = contests[i] contest = contests[i]
users, problems = get_contest_ranking_list(request, contest) users, problems = get_contest_ranking_list(None, contest)
for rank, user in users: for rank, user in users:
curr_score = 0 curr_score = 0
if rank - 1 < len(scores_system): if rank - 1 < len(scores_system):
@ -1452,7 +1585,9 @@ def contests_summary_view(request, key):
sorted_total_points = [ sorted_total_points = [
ContestsSummaryData( ContestsSummaryData(
user=user, username=user.username,
first_name=user.first_name,
last_name=user.last_name,
points=total_points[user], points=total_points[user],
point_contests=result_per_contest[user], point_contests=result_per_contest[user],
css_class=user_css_class[user], css_class=user_css_class[user],
@ -1462,17 +1597,68 @@ def contests_summary_view(request, key):
sorted_total_points.sort(key=lambda x: x.points, reverse=True) sorted_total_points.sort(key=lambda x: x.points, reverse=True)
total_rank = ranker(sorted_total_points) total_rank = ranker(sorted_total_points)
return [(rank, item._asdict()) for rank, item in total_rank]
context = {
"total_rank": list(total_rank),
"title": _("Contests Summary"),
"contests": contests,
}
cache.set(cache_key, context)
return render(request, "contest/contests_summary.html", context)
@receiver([post_save, post_delete], sender=ContestsSummary) class OfficialContestList(ContestList):
def clear_cache(sender, instance, **kwargs): official = True
cache.delete("csv:" + instance.key) template_name = "contest/official_list.html"
def setup_contest_list(self, request):
self.contest_query = request.GET.get("contest", "")
self.org_query = []
self.hide_organization_contests = False
self.selected_categories = []
self.selected_locations = []
self.year_from = None
self.year_to = None
if "category" in request.GET:
try:
self.selected_categories = list(
map(int, request.GET.getlist("category"))
)
except ValueError:
pass
if "location" in request.GET:
try:
self.selected_locations = list(
map(int, request.GET.getlist("location"))
)
except ValueError:
pass
if "year_from" in request.GET:
try:
self.year_from = int(request.GET.get("year_from"))
except ValueError:
pass
if "year_to" in request.GET:
try:
self.year_to = int(request.GET.get("year_to"))
except ValueError:
pass
def extra_queryset_filters(self, queryset):
if self.selected_categories:
queryset = queryset.filter(official__category__in=self.selected_categories)
if self.selected_locations:
queryset = queryset.filter(official__location__in=self.selected_locations)
if self.year_from:
queryset = queryset.filter(official__year__gte=self.year_from)
if self.year_to:
queryset = queryset.filter(official__year__lte=self.year_to)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_type"] = "official"
context["is_official"] = True
context["categories"] = OfficialContestCategory.objects.all()
context["locations"] = OfficialContestLocation.objects.all()
context["selected_categories"] = self.selected_categories
context["selected_locations"] = self.selected_locations
context["year_from"] = self.year_from
context["year_to"] = self.year_to
return context

View file

@ -1,24 +1,68 @@
from django.utils.html import mark_safe
from django.db import models from django.db import models
from judge.models.course import Course from django.views.generic import ListView, DetailView, View
from django.views.generic import ListView from django.utils.translation import gettext, gettext_lazy as _
from django.http import Http404
from django import forms
from django.forms import inlineformset_factory
from django.views.generic.edit import FormView
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.db.models import Max, F
from django.core.exceptions import ObjectDoesNotExist
__all__ = [ from judge.models import Course, CourseLesson, Submission, Profile, CourseRole
"CourseList", from judge.models.course import RoleInCourse
"CourseDetail", from judge.widgets import HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget
"CourseResource", from judge.utils.problems import (
"CourseResourceDetail", user_attempted_ids,
"CourseStudentResults", user_completed_ids,
"CourseEdit", )
"CourseResourceDetailEdit",
"CourseResourceEdit",
]
course_directory_file = ""
class CourseListMixin(object): def max_case_points_per_problem(profile, problems):
def get_queryset(self): # return a dict {problem_id: {case_points, case_total}}
return Course.objects.filter(is_open="true").values() q = (
Submission.objects.filter(user=profile, problem__in=problems)
.values("problem")
.annotate(case_points=Max("case_points"), case_total=F("case_total"))
.order_by("problem")
)
res = {}
for problem in q:
res[problem["problem"]] = problem
return res
def calculate_lessons_progress(profile, lessons):
res = {}
total_achieved_points = 0
total_points = 0
for lesson in lessons:
problems = list(lesson.problems.all())
if not problems:
res[lesson.id] = {"achieved_points": 0, "percentage": 0}
total_points += lesson.points
continue
problem_points = max_case_points_per_problem(profile, problems)
num_problems = len(problems)
percentage = 0
for val in problem_points.values():
score = val["case_points"] / val["case_total"]
percentage += score / num_problems
res[lesson.id] = {
"achieved_points": percentage * lesson.points,
"percentage": percentage * 100,
}
total_achieved_points += percentage * lesson.points
total_points += lesson.points
res["total"] = {
"achieved_points": total_achieved_points,
"total_points": total_points,
"percentage": total_achieved_points / total_points * 100 if total_points else 0,
}
return res
class CourseList(ListView): class CourseList(ListView):
@ -28,12 +72,179 @@ class CourseList(ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CourseList, self).get_context_data(**kwargs) context = super(CourseList, self).get_context_data(**kwargs)
available, enrolling = [], [] context["courses"] = Course.get_accessible_courses(self.request.profile)
for course in Course.objects.filter(is_public=True).filter(is_open=True): context["title"] = _("Courses")
if Course.is_accessible_by(course, self.request.profile): context["page_type"] = "list"
enrolling.append(course) return context
else:
available.append(course)
context["available"] = available class CourseDetailMixin(object):
context["enrolling"] = enrolling def dispatch(self, request, *args, **kwargs):
self.course = get_object_or_404(Course, slug=self.kwargs["slug"])
if not Course.is_accessible_by(self.course, self.request.profile):
raise Http404()
self.is_editable = Course.is_editable_by(self.course, self.request.profile)
return super(CourseDetailMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(CourseDetailMixin, self).get_context_data(**kwargs)
context["course"] = self.course
context["is_editable"] = self.is_editable
return context
class CourseEditableMixin(CourseDetailMixin):
def dispatch(self, request, *args, **kwargs):
res = super(CourseEditableMixin, self).dispatch(request, *args, **kwargs)
if not self.is_editable:
raise Http404()
return res
class CourseDetail(CourseDetailMixin, DetailView):
model = Course
template_name = "course/course.html"
def get_object(self):
return self.course
def get_context_data(self, **kwargs):
context = super(CourseDetail, self).get_context_data(**kwargs)
lessons = self.course.lessons.prefetch_related("problems").all()
context["title"] = self.course.name
context["page_type"] = "home"
context["lessons"] = lessons
context["lesson_progress"] = calculate_lessons_progress(
self.request.profile, lessons
)
return context
class CourseLessonDetail(CourseDetailMixin, DetailView):
model = CourseLesson
template_name = "course/lesson.html"
def get_object(self):
try:
self.lesson = CourseLesson.objects.get(
course=self.course, id=self.kwargs["id"]
)
return self.lesson
except ObjectDoesNotExist:
raise Http404()
def get_profile(self):
username = self.request.GET.get("user")
if not username:
return self.request.profile
is_editable = Course.is_editable_by(self.course, self.request.profile)
if not is_editable:
raise Http404()
try:
profile = Profile.objects.get(user__username=username)
is_student = profile.course_roles.filter(
role=RoleInCourse.STUDENT, course=self.course
).exists()
if not is_student:
raise Http404()
return profile
except ObjectDoesNotExist:
raise Http404()
def get_context_data(self, **kwargs):
context = super(CourseLessonDetail, self).get_context_data(**kwargs)
profile = self.get_profile()
context["title"] = self.lesson.title
context["lesson"] = self.lesson
context["completed_problem_ids"] = user_completed_ids(profile)
context["attempted_problems"] = user_attempted_ids(profile)
context["problem_points"] = max_case_points_per_problem(
profile, self.lesson.problems.all()
)
return context
class CourseLessonForm(forms.ModelForm):
class Meta:
model = CourseLesson
fields = ["order", "title", "points", "content", "problems"]
widgets = {
"title": forms.TextInput(),
"content": HeavyPreviewPageDownWidget(preview=reverse_lazy("blog_preview")),
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
}
CourseLessonFormSet = inlineformset_factory(
Course, CourseLesson, form=CourseLessonForm, extra=1, can_delete=True
)
class EditCourseLessonsView(CourseEditableMixin, FormView):
template_name = "course/edit_lesson.html"
form_class = CourseLessonFormSet
def get_context_data(self, **kwargs):
context = super(EditCourseLessonsView, self).get_context_data(**kwargs)
if self.request.method == "POST":
context["formset"] = self.form_class(
self.request.POST, self.request.FILES, instance=self.course
)
else:
context["formset"] = self.form_class(
instance=self.course, queryset=self.course.lessons.order_by("order")
)
context["title"] = _("Edit lessons for %(course_name)s") % {
"course_name": self.course.name
}
context["content_title"] = mark_safe(
_("Edit lessons for <a href='%(url)s'>%(course_name)s</a>")
% {
"course_name": self.course.name,
"url": self.course.get_absolute_url(),
}
)
context["page_type"] = "edit_lesson"
return context
def post(self, request, *args, **kwargs):
formset = self.form_class(request.POST, instance=self.course)
if formset.is_valid():
formset.save()
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def get_success_url(self):
return self.request.path
class CourseStudentResults(CourseEditableMixin, DetailView):
model = Course
template_name = "course/grades.html"
def get_object(self):
return self.course
def get_grades(self):
students = self.course.get_students()
students.sort(key=lambda u: u.username.lower())
lessons = self.course.lessons.prefetch_related("problems").all()
grades = {s: calculate_lessons_progress(s, lessons) for s in students}
return grades
def get_context_data(self, **kwargs):
context = super(CourseStudentResults, self).get_context_data(**kwargs)
context["title"] = mark_safe(
_("Grades in <a href='%(url)s'>%(course_name)s</a>")
% {
"course_name": self.course.name,
"url": self.course.get_absolute_url(),
}
)
context["page_type"] = "grades"
context["grades"] = self.get_grades()
return context return context

View file

@ -0,0 +1,43 @@
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage
from django import forms
from django.utils.translation import gettext as _
from django.conf import settings
from django.http import Http404
import os
import secrets
from urllib.parse import urljoin
MEDIA_PATH = "uploads"
class FileUploadForm(forms.Form):
file = forms.FileField()
def file_upload(request):
if not request.user.is_superuser:
raise Http404()
file_url = None
if request.method == "POST":
form = FileUploadForm(request.POST, request.FILES)
if form.is_valid():
file = request.FILES["file"]
random_str = secrets.token_urlsafe(5)
file_name, file_extension = os.path.splitext(file.name)
new_filename = f"{file_name}_{random_str}{file_extension}"
fs = FileSystemStorage(
location=os.path.join(settings.MEDIA_ROOT, MEDIA_PATH)
)
filename = fs.save(new_filename, file)
file_url = urljoin(settings.MEDIA_URL, os.path.join(MEDIA_PATH, filename))
else:
form = FileUploadForm()
return render(
request,
"custom_file_upload.html",
{"form": form, "file_url": file_url, "title": _("File Upload")},
)

View file

@ -2,24 +2,27 @@ from django.contrib.auth.decorators import login_required
from django.views.generic import ListView from django.views.generic import ListView
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.timezone import now from django.utils.timezone import now
from django.http import Http404
from judge.models import Profile, Notification, NotificationProfile from judge.models import Profile, Notification, NotificationProfile
from judge.models.notification import unseen_notifications_count from judge.models.notification import unseen_notifications_count
from judge.utils.infinite_paginator import InfinitePaginationMixin
__all__ = ["NotificationList"] __all__ = ["NotificationList"]
class NotificationList(ListView): class NotificationList(InfinitePaginationMixin, ListView):
model = Notification model = Notification
context_object_name = "notifications" context_object_name = "notifications"
template_name = "notification/list.html" template_name = "notification/list.html"
paginate_by = 50
def get_queryset(self): def get_queryset(self):
self.unseen_cnt = unseen_notifications_count(self.request.profile) self.unseen_cnt = unseen_notifications_count(self.request.profile)
self.queryset = Notification.objects.filter( self.queryset = Notification.objects.filter(
owner=self.request.profile owner=self.request.profile
).order_by("-id")[:100] ).order_by("-id")
return self.queryset return self.queryset
@ -27,11 +30,13 @@ class NotificationList(ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
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["first_page_href"] = "."
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
ret = super().get(request, *args, **kwargs) ret = super().get(request, *args, **kwargs)
if not request.user.is_authenticated:
raise Http404()
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0) NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
unseen_notifications_count.dirty(self.request.profile) unseen_notifications_count.dirty(self.request.profile)
return ret return ret

View file

@ -71,7 +71,7 @@ from judge.utils.views import (
from judge.utils.problems import user_attempted_ids, user_completed_ids 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 SubmissionsListBase
from judge.views.feed import FeedView from judge.views.feed import FeedView
from judge.tasks import rescore_contest from judge.tasks import rescore_contest
@ -104,15 +104,15 @@ class OrganizationBase(object):
def is_member(self, org=None): def is_member(self, org=None):
if org is None: if org is None:
org = self.object org = self.object
return ( if self.request.profile:
self.request.profile in org if self.request.user.is_authenticated else False return org.is_member(self.request.profile)
) return False
def is_admin(self, org=None): def is_admin(self, org=None):
if org is None: if org is None:
org = self.object org = self.object
if self.request.profile: if self.request.profile:
return org.admins.filter(id=self.request.profile.id).exists() return org.is_admin(self.request.profile)
return False return False
def can_access(self, org): def can_access(self, org):
@ -131,6 +131,13 @@ class OrganizationMixin(OrganizationBase):
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
context["organization_subdomain"] = (
("http" if settings.DMOJ_SSL == 0 else "https")
+ "://"
+ self.organization.slug
+ "."
+ get_current_site(self.request).domain
)
if "organizations" in context: if "organizations" in context:
context.pop("organizations") context.pop("organizations")
return context return context
@ -215,41 +222,103 @@ class OrganizationHomeView(OrganizationMixin):
organizations=self.organization, organizations=self.organization,
authors=self.request.profile, authors=self.request.profile,
).count() ).count()
context["top_rated"] = self.organization.members.filter( context["top_rated"] = (
is_unlisted=False self.organization.members.filter(is_unlisted=False)
).order_by("-rating")[:10] .order_by("-rating")
context["top_scorer"] = self.organization.members.filter( .only("id", "rating")[:10]
is_unlisted=False )
).order_by("-performance_points")[:10] context["top_scorer"] = (
self.organization.members.filter(is_unlisted=False)
.order_by("-performance_points")
.only("id", "performance_points")[:10]
)
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
return context return context
class OrganizationList(TitleMixin, ListView, OrganizationBase): class OrganizationList(
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView, OrganizationBase
):
model = Organization model = Organization
context_object_name = "organizations" context_object_name = "organizations"
template_name = "organization/list.html" template_name = "organization/list.html"
title = gettext_lazy("Groups") title = gettext_lazy("Groups")
paginate_by = 12
all_sorts = frozenset(("name", "member_count"))
default_desc = frozenset(("name", "member_count"))
def get_queryset(self): def get_default_sort_order(self, request):
return ( return "-member_count"
def get(self, request, *args, **kwargs):
default_tab = "mine"
if not self.request.user.is_authenticated:
default_tab = "public"
self.current_tab = self.request.GET.get("tab", default_tab)
self.organization_query = request.GET.get("organization", "")
return super(OrganizationList, self).get(request, *args, **kwargs)
def _get_queryset(self):
queryset = (
super(OrganizationList, self) super(OrganizationList, self)
.get_queryset() .get_queryset()
.annotate(member_count=Count("member")) .annotate(member_count=Count("member"))
.defer("about")
) )
if self.organization_query:
queryset = queryset.filter(
Q(slug__icontains=self.organization_query)
| Q(name__icontains=self.organization_query)
| Q(short_name__icontains=self.organization_query)
)
return queryset
def get_queryset(self):
organization_list = self._get_queryset()
my_organizations = []
if self.request.profile:
my_organizations = organization_list.filter(
id__in=self.request.profile.organizations.values("id")
)
if self.current_tab == "public":
queryset = organization_list.exclude(id__in=my_organizations).filter(
is_open=True
)
elif self.current_tab == "private":
queryset = organization_list.exclude(id__in=my_organizations).filter(
is_open=False
)
else:
queryset = my_organizations
if queryset:
queryset = queryset.order_by(self.order)
return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationList, self).get_context_data(**kwargs) context = super(OrganizationList, self).get_context_data(**kwargs)
context["my_organizations"] = []
context["page_type"] = "organizations" context["first_page_href"] = "."
if self.request.profile: context["current_tab"] = self.current_tab
context["my_organizations"] = context["organizations"].filter( context["page_type"] = self.current_tab
id__in=self.request.profile.organizations.values("id") context["organization_query"] = self.organization_query
) context["selected_order"] = self.request.GET.get("order")
other_organizations = context["organizations"].exclude( context["all_sort_options"] = [
id__in=context["my_organizations"] ("name", _("Name (asc.)")),
) ("-name", _("Name (desc.)")),
context["open_organizations"] = other_organizations.filter(is_open=True) ("member_count", _("Member count (asc.)")),
context["private_organizations"] = other_organizations.filter(is_open=False) ("-member_count", _("Member count (desc.)")),
]
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context return context
@ -274,14 +343,6 @@ class OrganizationHome(OrganizationHomeView, FeedView):
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.organization.name context["title"] = self.organization.name
http = "http" if settings.DMOJ_SSL == 0 else "https"
context["organization_subdomain"] = (
http
+ "://"
+ self.organization.slug
+ "."
+ get_current_site(self.request).domain
)
now = timezone.now() now = timezone.now()
visible_contests = ( visible_contests = (
@ -407,6 +468,7 @@ class OrganizationContests(
def get_queryset(self): def get_queryset(self):
self.org_query = [self.organization_id] self.org_query = [self.organization_id]
self.hide_organization_contests = False
return super().get_queryset() return super().get_queryset()
def set_editable_contest(self, contest): def set_editable_contest(self, contest):
@ -417,21 +479,20 @@ class OrganizationContests(
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationContests, self).get_context_data(**kwargs) context = super(OrganizationContests, self).get_context_data(**kwargs)
context["page_type"] = "contests" context["page_type"] = "contests"
context["hide_contest_orgs"] = True
context.pop("organizations") context.pop("organizations")
context["create_url"] = reverse(
"organization_contest_add",
args=[self.organization.id, self.organization.slug],
)
for participation in context["active_participations"]: if self.can_edit_organization(self.organization):
self.set_editable_contest(participation.contest) context["create_url"] = reverse(
for contest in context["past_contests"]: "organization_contest_add",
self.set_editable_contest(contest) args=[self.organization.id, self.organization.slug],
for contest in context["current_contests"]: )
self.set_editable_contest(contest)
for contest in context["future_contests"]: if self.current_tab == "active":
self.set_editable_contest(contest) for participation in context["contests"]:
self.set_editable_contest(participation.contest)
else:
for contest in context["contests"]:
self.set_editable_contest(contest)
return context return context
@ -471,6 +532,9 @@ class OrganizationSubmissions(
), ),
) )
def get_title(self):
return _("Submissions in") + f" {self.organization}"
class OrganizationMembershipChange( class OrganizationMembershipChange(
LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
@ -516,6 +580,7 @@ class JoinOrganization(OrganizationMembershipChange):
profile.organizations.add(org) profile.organizations.add(org)
profile.save() profile.save()
cache.delete(make_template_fragment_key("org_member_count", (org.id,))) cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
Organization.is_member.dirty(org, profile)
class LeaveOrganization(OrganizationMembershipChange): class LeaveOrganization(OrganizationMembershipChange):
@ -528,6 +593,7 @@ class LeaveOrganization(OrganizationMembershipChange):
) )
profile.organizations.remove(org) profile.organizations.remove(org)
cache.delete(make_template_fragment_key("org_member_count", (org.id,))) cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
Organization.is_member.dirty(org, profile)
class OrganizationRequestForm(Form): class OrganizationRequestForm(Form):
@ -737,7 +803,7 @@ class AddOrganizationMember(
def form_valid(self, form): def form_valid(self, form):
new_users = form.cleaned_data["new_users"] new_users = form.cleaned_data["new_users"]
self.object.members.add(*new_users) self.object.members.add(*new_users)
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
revisions.set_comment(_("Added members from site")) revisions.set_comment(_("Added members from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
return super(AddOrganizationMember, self).form_valid(form) return super(AddOrganizationMember, self).form_valid(form)
@ -804,7 +870,7 @@ class EditOrganization(
return form return form
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
revisions.set_comment(_("Edited from site")) revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form) return super(EditOrganization, self).form_valid(form)
@ -836,7 +902,7 @@ class AddOrganization(LoginRequiredMixin, TitleMixin, CreateView):
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD, % settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
status=400, status=400,
) )
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
revisions.set_comment(_("Added from site")) revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
res = super(AddOrganization, self).form_valid(form) res = super(AddOrganization, self).form_valid(form)
@ -861,7 +927,7 @@ class AddOrganizationContest(
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
revisions.set_comment(_("Added from site")) revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
@ -954,7 +1020,7 @@ class EditOrganizationContest(
return self.contest return self.contest
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
revisions.set_comment(_("Edited from site")) revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
res = super(EditOrganizationContest, self).form_valid(form) res = super(EditOrganizationContest, self).form_valid(form)
@ -974,6 +1040,18 @@ class EditOrganizationContest(
) )
): ):
transaction.on_commit(rescore_contest.s(self.object.key).delay) transaction.on_commit(rescore_contest.s(self.object.key).delay)
if any(
f in form.changed_data
for f in (
"authors",
"curators",
"testers",
)
):
Contest._author_ids.dirty(self.object)
Contest._curator_ids.dirty(self.object)
Contest._tester_ids.dirty(self.object)
return res return res
def get_problem_formset(self, post=False): def get_problem_formset(self, post=False):
@ -1015,7 +1093,7 @@ class AddOrganizationBlog(
return _("Add blog for %s") % self.organization.name return _("Add blog for %s") % self.organization.name
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
res = super(AddOrganizationBlog, self).form_valid(form) res = super(AddOrganizationBlog, self).form_valid(form)
self.object.is_organization_private = True self.object.is_organization_private = True
self.object.authors.add(self.request.profile) self.object.authors.add(self.request.profile)
@ -1038,6 +1116,11 @@ class AddOrganizationBlog(
) )
return res return res
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
class EditOrganizationBlog( class EditOrganizationBlog(
LoginRequiredMixin, LoginRequiredMixin,
@ -1061,7 +1144,7 @@ class EditOrganizationBlog(
if self.organization not in self.blog.organizations.all(): if self.organization not in self.blog.organizations.all():
raise Exception("This blog does not belong to this organization") raise Exception("This blog does not belong to this organization")
if ( if (
self.request.profile not in self.blog.authors.all() self.request.profile.id not in self.blog.get_authors()
and not self.can_edit_organization(self.organization) and not self.can_edit_organization(self.organization)
): ):
raise Exception("Not allowed to edit this blog") raise Exception("Not allowed to edit this blog")
@ -1115,13 +1198,18 @@ class EditOrganizationBlog(
make_notification(posible_users, action, html, self.request.profile) make_notification(posible_users, action, html, self.request.profile)
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with revisions.create_revision():
res = super(EditOrganizationBlog, self).form_valid(form) res = super(EditOrganizationBlog, self).form_valid(form)
revisions.set_comment(_("Edited from site")) revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
self.create_notification("Edit blog") self.create_notification("Edit blog")
return res return res
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
class PendingBlogs( class PendingBlogs(
LoginRequiredMixin, LoginRequiredMixin,

View file

@ -8,13 +8,13 @@ from django.http import (
HttpResponseForbidden, HttpResponseForbidden,
) )
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from judge.models.pagevote import PageVote, PageVoteVoter
from django.views.generic.base import TemplateResponseMixin from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from judge.dblock import LockModel
from django.views.generic import View, ListView from django.views.generic import View, ListView
from django_ratelimit.decorators import ratelimit
from django.conf import settings
from judge.models.pagevote import PageVote, PageVoteVoter, dirty_pagevote
__all__ = [ __all__ = [
"upvote_page", "upvote_page",
@ -24,6 +24,7 @@ __all__ = [
] ]
@ratelimit(key="user", rate=settings.RL_VOTE)
@login_required @login_required
def vote_page(request, delta): def vote_page(request, delta):
if abs(delta) != 1: if abs(delta) != 1:
@ -52,35 +53,33 @@ def vote_page(request, delta):
pagevote_id = int(request.POST["id"]) pagevote_id = int(request.POST["id"])
except ValueError: except ValueError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else:
if not PageVote.objects.filter(id=pagevote_id).exists(): try:
raise Http404() pagevote = PageVote.objects.get(id=pagevote_id)
except PageVote.DoesNotExist:
raise Http404()
vote = PageVoteVoter() vote = PageVoteVoter()
vote.pagevote_id = pagevote_id vote.pagevote_id = pagevote_id
vote.voter = request.profile vote.voter = request.profile
vote.score = delta vote.score = delta
while True: try:
vote.save()
except IntegrityError:
try: try:
vote.save() vote = PageVoteVoter.objects.get(
except IntegrityError: pagevote_id=pagevote_id, voter=request.profile
with LockModel(write=(PageVoteVoter,)):
try:
vote = PageVoteVoter.objects.get(
pagevote_id=pagevote_id, voter=request.profile
)
except PageVoteVoter.DoesNotExist:
# We must continue racing in case this is exploited to manipulate votes.
continue
vote.delete()
PageVote.objects.filter(id=pagevote_id).update(
score=F("score") - vote.score
) )
else: except PageVoteVoter.DoesNotExist:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) raise Http404()
break vote.delete()
_dirty_vote_score(pagevote_id, request.profile) PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
dirty_pagevote(pagevote, request.profile)
return HttpResponse("success", content_type="text/plain") return HttpResponse("success", content_type="text/plain")
@ -104,8 +103,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
context = super(PageVoteDetailView, self).get_context_data(**kwargs) context = super(PageVoteDetailView, self).get_context_data(**kwargs)
context["pagevote"] = self.object.get_or_create_pagevote() context["pagevote"] = self.object.get_or_create_pagevote()
return context return context
def _dirty_vote_score(pagevote_id, profile):
pv = PageVote(id=pagevote_id)
pv.vote_score.dirty(pv, profile)

View file

@ -1,10 +1,8 @@
import logging import logging
import os import os
import shutil import shutil
from datetime import timedelta, datetime
from operator import itemgetter from operator import itemgetter
from random import randrange from random import randrange
import random
from copy import deepcopy from copy import deepcopy
from django.core.cache import cache from django.core.cache import cache
@ -24,6 +22,7 @@ from django.db.models import (
Q, Q,
When, When,
IntegerField, IntegerField,
Sum,
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.db.utils import ProgrammingError from django.db.utils import ProgrammingError
@ -46,7 +45,7 @@ from django.views.generic import ListView, View
from django.views.generic.base import TemplateResponseMixin from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from judge.comments import CommentedDetailView from judge.views.comment import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import ( from judge.models import (
ContestProblem, ContestProblem,
@ -66,6 +65,7 @@ from judge.models import (
Organization, Organization,
Profile, Profile,
LanguageTemplate, LanguageTemplate,
Contest,
) )
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
@ -77,6 +77,8 @@ from judge.utils.problems import (
user_attempted_ids, user_attempted_ids,
user_completed_ids, user_completed_ids,
get_related_problems, get_related_problems,
get_user_recommended_problems,
RecommendationType,
) )
from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter from judge.utils.tickets import own_ticket_filter
@ -351,7 +353,7 @@ 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: if not self.in_contest and settings.ML_OUTPUT_PATH:
context["related_problems"] = get_related_problems( context["related_problems"] = get_related_problems(
self.profile, self.object self.profile, self.object
) )
@ -399,16 +401,13 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
if trans is None if trans is None
else trans.description, else trans.description,
"url": request.build_absolute_uri(), "url": request.build_absolute_uri(),
"math_engine": maker.math_engine,
} }
) )
.replace('"//', '"https://') .replace('"//', '"https://')
.replace("'//", "'https://") .replace("'//", "'https://")
) )
maker.title = problem_name maker.title = problem_name
assets = ["style.css", "pygment-github.css"] assets = ["style.css"]
if maker.math_engine == "jax":
assets.append("mathjax3_config.js")
for file in assets: for file in assets:
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make() maker.make()
@ -590,7 +589,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
i i
for i in query for i in query
if i in self.profile.organizations.values_list("id", flat=True) if i in self.profile.organizations.values_list("id", flat=True)
] ][:3]
def get_normal_queryset(self): def get_normal_queryset(self):
queryset = Problem.get_visible_problems(self.request.user) queryset = Problem.get_visible_problems(self.request.user)
@ -602,9 +601,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
self.org_query = [self.request.organization.id] 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)
contest_problems = (
Contest.objects.filter(organizations__in=self.org_query)
.select_related("problems")
.values_list("contest_problems__problem__id")
.distinct()
)
queryset = queryset.filter( queryset = queryset.filter(
Q(organizations__in=self.org_query) Q(organizations__in=self.org_query) | Q(id__in=contest_problems)
| Q(contests__contest__organizations__in=self.org_query)
) )
if self.author_query: if self.author_query:
queryset = queryset.filter(authors__in=self.author_query) queryset = queryset.filter(authors__in=self.author_query)
@ -641,6 +645,16 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
queryset = queryset.filter(points__gte=self.point_start) queryset = queryset.filter(points__gte=self.point_start)
if self.point_end is not None: if self.point_end is not None:
queryset = queryset.filter(points__lte=self.point_end) queryset = queryset.filter(points__lte=self.point_end)
queryset = queryset.annotate(
has_public_editorial=Sum(
Case(
When(solution__is_public=True, then=1),
default=0,
output_field=IntegerField(),
)
)
)
return queryset.distinct() return queryset.distinct()
def get_queryset(self): def get_queryset(self):
@ -664,12 +678,6 @@ 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 = Problem.objects.values_list("authors", flat=True)
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:
@ -677,7 +685,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
context["problem_types"] = ProblemType.objects.all() context["problem_types"] = ProblemType.objects.all()
context["has_fts"] = settings.ENABLE_FTS context["has_fts"] = settings.ENABLE_FTS
context["org_query"] = self.org_query context["org_query"] = self.org_query
context["author_query"] = self.author_query context["author_query"] = Profile.objects.filter(id__in=self.author_query)
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()
@ -829,29 +837,39 @@ 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"
feed_content_template_name = "problem/feed/problems.html" feed_content_template_name = "problem/feed/items.html"
paginate_by = 4 paginate_by = 4
title = _("Problem feed") title = _("Problem feed")
feed_type = None feed_type = None
# arr = [[], [], ..] def get_recommended_problem_ids(self, queryset):
def merge_recommendation(self, arr): user_id = self.request.profile.id
seed = datetime.now().strftime("%d%m%Y") problem_ids = queryset.values_list("id", flat=True)
merged_array = [] rec_types = [
for a in arr: RecommendationType.CF_DOT,
merged_array += a RecommendationType.CF_COSINE,
random.Random(seed).shuffle(merged_array) RecommendationType.CF_TIME_DOT,
RecommendationType.CF_TIME_COSINE,
RecommendationType.HOT_PROBLEM,
]
limits = [100, 100, 100, 100, 20]
shuffle = True
res = [] allow_debug_type = (
used_pid = set() self.request.user.is_impersonate or self.request.user.is_superuser
)
if allow_debug_type and "debug_type" in self.request.GET:
try:
debug_type = int(self.request.GET.get("debug_type"))
except ValueError:
raise Http404()
rec_types = [debug_type]
limits = [100]
shuffle = False
for obj in merged_array: return get_user_recommended_problems(
if type(obj) == tuple: user_id, problem_ids, rec_types, limits, shuffle
obj = obj[1] )
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res
def get_queryset(self): def get_queryset(self):
if self.feed_type == "volunteer": if self.feed_type == "volunteer":
@ -885,40 +903,8 @@ class ProblemFeed(ProblemList, FeedView):
if not settings.ML_OUTPUT_PATH or not user: if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE) return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
cf_model = CollabFilter("collab_filter") q = self.get_recommended_problem_ids(queryset)
cf_time_model = CollabFilter("collab_filter_time")
queryset = queryset.values_list("id", flat=True)
hot_problems_recommendations = [
problem.id
for problem in hot_problems(timedelta(days=7), 20)
if problem.id in set(queryset)
]
q = self.merge_recommendation(
[
cf_model.user_recommendations(user, queryset, cf_model.DOT, 100),
cf_model.user_recommendations(
user,
queryset,
cf_model.COSINE,
100,
),
cf_time_model.user_recommendations(
user,
queryset,
cf_time_model.COSINE,
100,
),
cf_time_model.user_recommendations(
user,
queryset,
cf_time_model.DOT,
100,
),
hot_problems_recommendations,
]
)
queryset = Problem.objects.filter(id__in=q) queryset = Problem.objects.filter(id__in=q)
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
@ -974,6 +960,12 @@ class LanguageTemplateAjax(View):
class RandomProblem(ProblemList): class RandomProblem(ProblemList):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.setup_problem_list(request) self.setup_problem_list(request)
try:
return super().get(request, *args, **kwargs)
except ProgrammingError as e:
return generic_message(request, "FTS syntax error", e.args[1], status=400)
if self.in_contest: if self.in_contest:
raise Http404() raise Http404()
@ -994,6 +986,15 @@ class RandomProblem(ProblemList):
user_logger = logging.getLogger("judge.user") user_logger = logging.getLogger("judge.user")
def last_nth_submitted_date_in_contest(profile, contest, n):
submissions = Submission.objects.filter(
user=profile, contest_object=contest
).order_by("-id")[:n]
if submissions.count() >= n:
return submissions[n - 1].date
return None
@login_required @login_required
def problem_submit(request, problem, submission=None): def problem_submit(request, problem, submission=None):
if ( if (
@ -1042,7 +1043,7 @@ def problem_submit(request, problem, submission=None):
>= settings.DMOJ_SUBMISSION_LIMIT >= settings.DMOJ_SUBMISSION_LIMIT
): ):
return HttpResponse( return HttpResponse(
"<h1>You submitted too many submissions.</h1>", status=429 _("<h1>You have submitted too many submissions.</h1>"), status=429
) )
if not problem.allowed_languages.filter( if not problem.allowed_languages.filter(
id=form.cleaned_data["language"].id id=form.cleaned_data["language"].id
@ -1063,7 +1064,22 @@ def problem_submit(request, problem, submission=None):
with transaction.atomic(): with transaction.atomic():
if profile.current_contest is not None: if profile.current_contest is not None:
contest = profile.current_contest.contest
contest_id = profile.current_contest.contest_id contest_id = profile.current_contest.contest_id
rate_limit = contest.rate_limit
if rate_limit:
t = last_nth_submitted_date_in_contest(
profile, contest, rate_limit
)
if t is not None and timezone.now() - t < timezone.timedelta(
minutes=1
):
return HttpResponse(
_("<h1>You have submitted too many submissions.</h1>"),
status=429,
)
try: try:
contest_problem = problem.contests.get(contest_id=contest_id) contest_problem = problem.contests.get(contest_id=contest_id)
except ContestProblem.DoesNotExist: except ContestProblem.DoesNotExist:
@ -1143,11 +1159,11 @@ def problem_submit(request, problem, submission=None):
default_lang = request.profile.language default_lang = request.profile.language
submission_limit = submissions_left = None submission_limit = submissions_left = None
next_valid_submit_time = None
if profile.current_contest is not None: if profile.current_contest is not None:
contest = profile.current_contest.contest
try: try:
submission_limit = problem.contests.get( submission_limit = problem.contests.get(contest=contest).max_submissions
contest=profile.current_contest.contest
).max_submissions
except ContestProblem.DoesNotExist: except ContestProblem.DoesNotExist:
pass pass
else: else:
@ -1155,6 +1171,12 @@ def problem_submit(request, problem, submission=None):
submissions_left = submission_limit - get_contest_submission_count( submissions_left = submission_limit - get_contest_submission_count(
problem, profile, profile.current_contest.virtual problem, profile, profile.current_contest.virtual
) )
if contest.rate_limit:
t = last_nth_submitted_date_in_contest(profile, contest, contest.rate_limit)
if t is not None:
next_valid_submit_time = t + timezone.timedelta(minutes=1)
next_valid_submit_time = next_valid_submit_time.isoformat()
return render( return render(
request, request,
"problem/submit.html", "problem/submit.html",
@ -1184,6 +1206,7 @@ def problem_submit(request, problem, submission=None):
"output_only": problem.data_files.output_only "output_only": problem.data_files.output_only
if hasattr(problem, "data_files") if hasattr(problem, "data_files")
else False, else False,
"next_valid_submit_time": next_valid_submit_time,
}, },
) )
@ -1208,7 +1231,7 @@ class ProblemClone(
problem.ac_rate = 0 problem.ac_rate = 0
problem.user_count = 0 problem.user_count = 0
problem.code = form.cleaned_data["code"] problem.code = form.cleaned_data["code"]
problem.save() problem.save(should_move_data=False)
problem.authors.add(self.request.profile) problem.authors.add(self.request.profile)
problem.allowed_languages.set(languages) problem.allowed_languages.set(languages)
problem.language_limits.set(language_limits) problem.language_limits.set(language_limits)

View file

@ -89,7 +89,7 @@ class ProblemDataForm(ModelForm):
"checker", "checker",
"checker_args", "checker_args",
"custom_checker", "custom_checker",
"custom_validator", "custom_checker_cpp",
"interactive_judge", "interactive_judge",
"fileio_input", "fileio_input",
"fileio_output", "fileio_output",
@ -344,7 +344,7 @@ def problem_init_view(request, problem):
"problem/yaml.html", "problem/yaml.html",
{ {
"raw_source": data, "raw_source": data,
"highlighted_source": highlight_code(data, "yaml", linenos=False), "highlighted_source": highlight_code(data, "yaml", linenos=True),
"title": _("Generated init.yml for %s") % problem.name, "title": _("Generated init.yml for %s") % problem.name,
"content_title": mark_safe( "content_title": mark_safe(
escape(_("Generated init.yml for %s")) escape(_("Generated init.yml for %s"))

View file

@ -78,12 +78,12 @@ class ManageProblemSubmissionView(TitleMixin, ManageProblemSubmissionMixin, Deta
) )
] ]
context["results"] = sorted(map(itemgetter(0), Submission.RESULT)) context["results"] = sorted(map(itemgetter(0), Submission.RESULT))
context["in_contest"] = False context["current_contest"] = None
if ( if (
self.request.in_contest_mode self.request.in_contest_mode
and self.object in self.request.participation.contest.problems.all() and self.object in self.request.participation.contest.problems.all()
): ):
context["in_contest"] = True context["current_contest"] = self.request.participation.contest
return context return context
@ -106,20 +106,12 @@ class BaseActionSubmissionsView(
try: try:
languages = list(map(int, self.request.POST.getlist("language"))) languages = list(map(int, self.request.POST.getlist("language")))
results = list(map(str, self.request.POST.getlist("result")))
contests = list(map(int, self.request.POST.getlist("contest")))
except ValueError: except ValueError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
contest = None return self.generate_response(id_range, languages, results, contests)
try:
in_contest = bool(self.request.POST.get("in_contest", False))
if in_contest:
contest = self.request.participation.contest
except (KeyError, ValueError):
return HttpResponseBadRequest()
return self.generate_response(
id_range, languages, self.request.POST.getlist("result"), contest
)
def generate_response(self, id_range, languages, results, contest): def generate_response(self, id_range, languages, results, contest):
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,6 +1,6 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden, JsonResponse
from judge.models import Contest from judge.models import Contest
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -21,7 +21,7 @@ class Resolver(TemplateView):
hidden_subtasks = self.contest.format.get_hidden_subtasks() hidden_subtasks = self.contest.format.get_hidden_subtasks()
num_problems = len(problems) num_problems = len(problems)
problem_sub = [0] * num_problems problem_sub = [0] * num_problems
sub_frozen = [0] * num_problems sub_frozen = [[] for _ in range(num_problems)]
problems_json = {str(i): {} for i in range(1, num_problems + 1)} problems_json = {str(i): {} for i in range(1, num_problems + 1)}
users = {} users = {}
@ -126,10 +126,8 @@ class Resolver(TemplateView):
for i in hidden_subtasks: for i in hidden_subtasks:
order = id_to_order[i] order = id_to_order[i]
if hidden_subtasks[i]: sub_frozen[order - 1] = list(hidden_subtasks[i])
sub_frozen[order - 1] = min(hidden_subtasks[i])
else:
sub_frozen[order - 1] = problem_sub[order - 1] + 1
return { return {
"problem_sub": problem_sub, "problem_sub": problem_sub,
"sub_frozen": sub_frozen, "sub_frozen": sub_frozen,
@ -143,8 +141,15 @@ class Resolver(TemplateView):
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.user.is_superuser: if not request.user.is_superuser:
self.contest = Contest.objects.get(key=kwargs.get("contest")) return HttpResponseForbidden()
if self.contest.format.has_hidden_subtasks: self.contest = Contest.objects.get(key=kwargs.get("contest"))
return super(Resolver, self).get(request, *args, **kwargs) if not self.contest.format.has_hidden_subtasks:
return HttpResponseForbidden() return HttpResponseForbidden()
if self.request.GET.get("json"):
json_dumps_params = {"ensure_ascii": False}
return JsonResponse(
self.get_contest_json(), json_dumps_params=json_dumps_params
)
return super(Resolver, self).get(request, *args, **kwargs)

View file

@ -85,15 +85,17 @@ class ProblemSelect2View(Select2View):
class ContestSelect2View(Select2View): class ContestSelect2View(Select2View):
def get(self, request, *args, **kwargs):
self.problem_id = kwargs.get("problem_id", request.GET.get("problem_id", ""))
return super(ContestSelect2View, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
return Contest.get_visible_contests(self.request.user).filter( q = Contest.get_visible_contests(self.request.user).filter(
Q(key__icontains=self.term) | Q(name__icontains=self.term) Q(key__icontains=self.term) | Q(name__icontains=self.term)
) )
if self.problem_id:
q = q.filter(problems=self.problem_id)
class CommentSelect2View(Select2View): return q
def get_queryset(self):
return Comment.objects.filter(page__icontains=self.term)
class UserSearchSelect2View(BaseListView): class UserSearchSelect2View(BaseListView):
@ -193,3 +195,17 @@ class ChatUserSearchSelect2View(UserSearchSelect2View):
), ),
"display_rank": display_rank, "display_rank": display_rank,
} }
class ProblemAuthorSearchSelect2View(UserSearchSelect2View):
def get_queryset(self):
return Profile.objects.filter(
authored_problems__isnull=False, user__username__icontains=self.term
).distinct()
def get_json_result_from_object(self, user_tuple):
pk, username, email, display_rank, profile_image = user_tuple
return {
"text": username,
"id": pk,
}

View file

@ -33,29 +33,31 @@ from django.views import View
from judge import event_poster as event from judge import event_poster as event
from judge.highlight_code import highlight_code from judge.highlight_code import highlight_code
from judge.models import Contest, ContestParticipation from judge.models import (
from judge.models import Language Contest,
from judge.models import Problem ContestParticipation,
from judge.models import ProblemTestCase Language,
from judge.models import ProblemTranslation Problem,
from judge.models import Profile ProblemTestCase,
from judge.models import Submission ProblemTranslation,
Profile,
Submission,
)
from judge.utils.problems import get_result_data from judge.utils.problems import get_result_data
from judge.utils.problems import user_completed_ids, user_editable_ids, user_tester_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.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
from judge.views.contests import ContestMixin
from judge.caching import cache_wrapper
def submission_related(queryset): def submission_related(queryset):
return queryset.select_related("user__user", "problem", "language").only( return queryset.select_related("user", "problem", "language").only(
"id", "id",
"user__user__username", "user__id",
"user__display_rank",
"user__rating",
"problem__name", "problem__name",
"problem__code", "problem__code",
"problem__is_public", "problem__is_public",
@ -70,7 +72,8 @@ def submission_related(queryset):
"case_points", "case_points",
"case_total", "case_total",
"current_testcase", "current_testcase",
"contest_object", "contest_object__key",
"contest_object__name",
) )
@ -81,6 +84,10 @@ class SubmissionMixin(object):
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView): class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
queryset = Submission.objects.select_related(
"language", "problem", "user", "contest_object"
).defer("problem__description", "user__about", "contest_object__description")
def get_object(self, queryset=None): def get_object(self, queryset=None):
submission = super(SubmissionDetailBase, self).get_object(queryset) submission = super(SubmissionDetailBase, self).get_object(queryset)
if submission.is_accessible_by(self.request.profile): if submission.is_accessible_by(self.request.profile):
@ -92,7 +99,7 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta
submission = self.object submission = self.object
return _("Submission of %(problem)s by %(user)s") % { return _("Submission of %(problem)s by %(user)s") % {
"problem": submission.problem.translated_name(self.request.LANGUAGE_CODE), "problem": submission.problem.translated_name(self.request.LANGUAGE_CODE),
"user": submission.user.user.username, "user": submission.user.username,
} }
def get_content_title(self): def get_content_title(self):
@ -107,29 +114,13 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta
), ),
"user": format_html( "user": format_html(
'<a href="{0}">{1}</a>', '<a href="{0}">{1}</a>',
reverse("user_page", args=[submission.user.user.username]), reverse("user_page", args=[submission.user.username]),
submission.user.user.username, submission.user.username,
), ),
} }
) )
class SubmissionSource(SubmissionDetailBase):
template_name = "submission/source.html"
def get_queryset(self):
return super().get_queryset().select_related("source")
def get_context_data(self, **kwargs):
context = super(SubmissionSource, self).get_context_data(**kwargs)
submission = self.object
context["raw_source"] = submission.source.source.rstrip("\n")
context["highlighted_source"] = highlight_code(
submission.source.source, submission.language.pygments, linenos=False
)
return context
def get_hidden_subtasks(request, submission): def get_hidden_subtasks(request, submission):
contest = submission.contest_object contest = submission.contest_object
if contest and contest.is_editable_by(request.user): if contest and contest.is_editable_by(request.user):
@ -205,15 +196,28 @@ def get_cases_data(submission):
class SubmissionStatus(SubmissionDetailBase): class SubmissionStatus(SubmissionDetailBase):
template_name = "submission/status.html" template_name = "submission/status.html"
def access_testcases_in_contest(self): def can_see_testcases(self):
contest = self.object.contest_or_none contest_submission = self.object.contest_or_none
if contest is None: if contest_submission is None:
return False
if contest.problem.problem.is_editable_by(self.request.user):
return True return True
if contest.problem.contest.is_in_contest(self.request.user):
contest_problem = contest_submission.problem
problem = self.object.problem
contest = self.object.contest_object
if contest_problem.show_testcases:
return True
if problem.is_editable_by(self.request.user):
return True
if contest.is_editable_by(self.request.user):
return True
if not problem.is_public:
return False return False
if contest.participation.ended: if contest.is_in_contest(self.request.user):
return False
if not contest.ended:
return False
if contest_submission.participation.ended:
return True return True
return False return False
@ -228,19 +232,14 @@ class SubmissionStatus(SubmissionDetailBase):
) )
context["time_limit"] = submission.problem.time_limit context["time_limit"] = submission.problem.time_limit
context["can_see_testcases"] = False context["can_see_testcases"] = False
context["raw_source"] = submission.source.source.rstrip("\n")
context["highlighted_source"] = highlight_code( context["highlighted_source"] = highlight_code(
submission.source.source, submission.language.pygments, linenos=False submission.source.source,
submission.language.pygments,
linenos=True,
title=submission.language,
) )
contest = submission.contest_or_none if self.can_see_testcases():
show_testcases = False
can_see_testcases = self.access_testcases_in_contest()
if contest is not None:
show_testcases = contest.problem.show_testcases or False
if contest is None or show_testcases or can_see_testcases:
context["cases_data"] = get_cases_data(submission) context["cases_data"] = get_cases_data(submission)
context["can_see_testcases"] = True context["can_see_testcases"] = True
try: try:
@ -266,7 +265,7 @@ class SubmissionTestCaseQuery(SubmissionStatus):
return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs) return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs)
class SubmissionSourceRaw(SubmissionSource): class SubmissionSourceRaw(SubmissionDetailBase):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
submission = self.get_object() submission = self.get_object()
return HttpResponse(submission.source.source, content_type="text/plain") return HttpResponse(submission.source.source, content_type="text/plain")
@ -311,6 +310,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
def access_check(self, request): def access_check(self, request):
pass pass
def hide_contest_in_row(self):
return self.request.in_contest_mode
@cached_property @cached_property
def in_contest(self): def in_contest(self):
return ( return (
@ -379,17 +381,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
) )
if self.selected_languages: if self.selected_languages:
# Note (DMOJ): MariaDB can't optimize this subquery for some insane, unknown reason, queryset = queryset.filter(language__in=self.selected_languages)
# so we are forcing an eager evaluation to get the IDs right here.
# Otherwise, with multiple language filters, MariaDB refuses to use an index
# (or runs the subquery for every submission, which is even more horrifying to think about).
queryset = queryset.filter(
language__in=list(
Language.objects.filter(
key__in=self.selected_languages
).values_list("id", flat=True)
)
)
if self.selected_statuses: if self.selected_statuses:
submission_results = [i for i, _ in Submission.RESULT] submission_results = [i for i, _ in Submission.RESULT]
if self.selected_statuses[0] in submission_results: if self.selected_statuses[0] in submission_results:
@ -460,7 +452,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
context["show_problem"] = self.show_problem context["show_problem"] = self.show_problem
context["profile"] = self.request.profile context["profile"] = self.request.profile
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_key
context["all_statuses"] = self.get_searchable_status_codes() context["all_statuses"] = self.get_searchable_status_codes()
context["selected_statuses"] = self.selected_statuses context["selected_statuses"] = self.selected_statuses
@ -480,11 +472,16 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
context["friend_submissions_link"] = self.get_friend_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
context["hide_contest_in_row"] = self.hide_contest_in_row()
context["in_hidden_subtasks_contest"] = self.in_hidden_subtasks_contest() context["in_hidden_subtasks_contest"] = self.in_hidden_subtasks_contest()
if context["in_hidden_subtasks_contest"]: if context["in_hidden_subtasks_contest"]:
for submission in context["submissions"]: for submission in context["submissions"]:
self.modify_attrs(submission) self.modify_attrs(submission)
context[
"is_in_editable_contest"
] = self.in_contest and self.contest.is_editable_by(self.request.user)
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -494,6 +491,19 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
self.selected_languages = request.GET.getlist("language") self.selected_languages = request.GET.getlist("language")
self.selected_statuses = request.GET.getlist("status") self.selected_statuses = request.GET.getlist("status")
self.selected_languages_key = []
if self.selected_languages:
languages = Language.objects.filter(key__in=self.selected_languages).values(
"id", "key"
)
self.selected_languages = [i["id"] for i in languages]
self.selected_languages_key = [i["key"] for i in languages]
if self.selected_statuses:
allowed_statuses = [i for i, _ in Submission.RESULT + Submission.STATUS]
self.selected_statuses = [
i for i in self.selected_statuses if i in allowed_statuses
]
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
@ -736,6 +746,11 @@ def single_submission(request, submission_id, show_problem=True):
submission_related(Submission.objects.all()), id=int(submission_id) submission_related(Submission.objects.all()), id=int(submission_id)
) )
is_in_editable_contest = False
if authenticated and request.in_contest_mode:
contest = request.profile.current_contest.contest
is_in_editable_contest = contest.is_editable_by(request.user)
if not submission.problem.is_accessible_by(request.user): if not submission.problem.is_accessible_by(request.user):
raise Http404() raise Http404()
@ -748,6 +763,7 @@ def single_submission(request, submission_id, show_problem=True):
"problem_name": show_problem "problem_name": show_problem
and submission.problem.translated_name(request.LANGUAGE_CODE), and submission.problem.translated_name(request.LANGUAGE_CODE),
"profile": request.profile if authenticated else None, "profile": request.profile if authenticated else None,
"is_in_editable_contest": is_in_editable_contest,
}, },
) )
@ -783,28 +799,9 @@ class AllSubmissions(InfinitePaginationMixin, GeneralSubmissions):
if self.request.organization or self.in_contest: 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" return _get_global_submission_result_data(
if self.selected_statuses: self.selected_statuses, self.selected_languages
key += ":" + ",".join(self.selected_statuses) )
if self.selected_languages:
key += ":" + ",".join(self.selected_languages)
result = cache.get(key)
if result:
return result
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)
return result
class ForceContestMixin(object): class ForceContestMixin(object):
@ -842,6 +839,38 @@ class ForceContestMixin(object):
return super(ForceContestMixin, self).get(request, *args, **kwargs) return super(ForceContestMixin, self).get(request, *args, **kwargs)
class ContestSubmissions(
LoginRequiredMixin, ContestMixin, ForceContestMixin, SubmissionsListBase
):
check_contest_in_access_check = True
template_name = "contest/submissions.html"
context_object_name = "submissions"
def hide_contest_in_row(self):
return True
def access_check(self, request):
super().contest_access_check(self.contest)
super().access_check(request)
def get_title(self):
return _("Submissions in") + " " + self.contest.name
def get_content_title(self):
return format_html(
_('Submissions in <a href="{0}">{1}</a>'),
reverse("contest_view", args=[self.contest.key]),
self.contest.name,
)
def get_context_data(self, **kwargs):
self.object = self.contest
context = super(ContestSubmissions, self).get_context_data(**kwargs)
context["contest"] = self.contest
context["page_type"] = "submissions"
return context
class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions): class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions):
check_contest_in_access_check = True check_contest_in_access_check = True
@ -1027,3 +1056,19 @@ class SubmissionSourceFileView(View):
response["Content-Type"] = "application/octet-stream" response["Content-Type"] = "application/octet-stream"
response["Content-Disposition"] = "attachment; filename=%s" % (filename,) response["Content-Disposition"] = "attachment; filename=%s" % (filename,)
return response return response
@cache_wrapper(prefix="gsrd", timeout=3600, expected_type=dict)
def _get_global_submission_result_data(statuses, languages):
queryset = Submission.objects
if languages:
queryset = queryset.filter(
language__in=Language.objects.filter(id__in=languages)
)
if statuses:
submission_results = [i for i, _ in Submission.RESULT]
if statuses[0] in submission_results:
queryset = queryset.filter(result__in=statuses)
else:
queryset = queryset.filter(status__in=statuses)
return get_result_data(queryset)

View file

@ -0,0 +1,207 @@
from django.views import View
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.core.files import File
from django.core.files.base import ContentFile
from django.http import (
FileResponse,
HttpResponseRedirect,
HttpResponseBadRequest,
HttpResponse,
)
from judge.models import TestFormatterModel
from judge.forms import TestFormatterForm
from judge.views.test_formatter import tf_logic, tf_utils
from django.utils.translation import gettext_lazy as _
from zipfile import ZipFile, ZIP_DEFLATED
import os
import uuid
from dmoj import settings
def id_to_path(id):
return os.path.join(settings.MEDIA_ROOT, "test_formatter/" + id + "/")
def get_names_in_archive(file_path):
suffixes = ("inp", "out", "INP", "OUT")
with ZipFile(os.path.join(settings.MEDIA_ROOT, file_path)) as f:
result = [
x for x in f.namelist() if not x.endswith("/") and x.endswith(suffixes)
]
return list(sorted(result, key=tf_utils.natural_sorting_key))
def get_renamed_archive(file_str, file_name, file_path, bef, aft):
target_file_id = str(uuid.uuid4())
source_path = os.path.join(settings.MEDIA_ROOT, file_str)
target_path = os.path.join(settings.MEDIA_ROOT, file_str + "_" + target_file_id)
new_path = os.path.join(settings.MEDIA_ROOT, "test_formatter/" + file_name)
source = ZipFile(source_path, "r")
target = ZipFile(target_path, "w", ZIP_DEFLATED)
for bef_name, aft_name in zip(bef, aft):
target.writestr(aft_name, source.read(bef_name))
os.remove(source_path)
os.rename(target_path, new_path)
target.close()
source.close()
return {"file_path": "test_formatter/" + file_name}
class TestFormatter(View):
form_class = TestFormatterForm()
def get(self, request):
return render(
request,
"test_formatter/test_formatter.html",
{"title": _("Test Formatter"), "form": self.form_class},
)
def post(self, request):
form = TestFormatterForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect("edit_page")
return render(
request, "test_formatter/test_formatter.html", {"form": self.form_class}
)
class EditTestFormatter(View):
file_path = ""
def get(self, request):
file = TestFormatterModel.objects.last()
filestr = str(file.file)
filename = filestr.split("/")[-1]
filepath = filestr.split("/")[0]
bef_file = get_names_in_archive(filestr)
preview_data = {
"bef_inp_format": bef_file[0],
"bef_out_format": bef_file[1],
"aft_inp_format": "input.000",
"aft_out_format": "output.000",
"file_str": filestr,
}
preview = tf_logic.preview(preview_data)
response = ""
for i in range(len(bef_file)):
bef = preview["bef_preview"][i]["value"]
aft = preview["aft_preview"][i]["value"]
response = response + f"<p>{bef} => {aft}</p>\n"
return render(
request,
"test_formatter/edit_test_formatter.html",
{
"title": _("Test Formatter"),
"check": 0,
"files_list": bef_file,
"file_name": filename,
"res": response,
},
)
def post(self, request, *args, **kwargs):
action = request.POST.get("action")
if action == "convert":
try:
file = TestFormatterModel.objects.last()
filestr = str(file.file)
filename = filestr.split("/")[-1]
filepath = filestr.split("/")[0]
bef_inp_format = request.POST["bef_inp_format"]
bef_out_format = request.POST["bef_out_format"]
aft_inp_format = request.POST["aft_inp_format"]
aft_out_format = request.POST["aft_out_format"]
aft_file_name = request.POST["file_name"]
except KeyError:
return HttpResponseBadRequest("No data.")
if filename != aft_file_name:
source_path = os.path.join(settings.MEDIA_ROOT, filestr)
new_path = os.path.join(
settings.MEDIA_ROOT, "test_formatter/" + aft_file_name
)
os.rename(source_path, new_path)
filename = aft_file_name
preview_data = {
"bef_inp_format": bef_inp_format,
"bef_out_format": bef_out_format,
"aft_inp_format": aft_inp_format,
"aft_out_format": aft_out_format,
"file_name": filename,
"file_path": filepath,
"file_str": filepath + "/" + filename,
}
converted_zip = tf_logic.convert(preview_data)
global file_path
file_path = converted_zip["file_path"]
zip_instance = TestFormatterModel()
zip_instance.file = file_path
zip_instance.save()
preview = tf_logic.preview(preview_data)
response = HttpResponse()
for i in range(len(preview["bef_preview"])):
bef = preview["bef_preview"][i]["value"]
aft = preview["aft_preview"][i]["value"]
response.write(f"<p>{bef} => {aft}</p>")
return response
elif action == "download":
return HttpResponse(file_path)
return HttpResponseBadRequest("Invalid action")
class DownloadTestFormatter(View):
def get(self, request):
file_path = request.GET.get("file_path")
file_name = file_path.split("/")[-1]
preview_file = tf_logic.preview_file(file_path)
response = ""
for i in range(len(preview_file)):
response = response + (f"<p>{preview_file[i]}</p>\n")
files_list = [preview_file[0], preview_file[1]]
return render(
request,
"test_formatter/download_test_formatter.html",
{
"title": _("Test Formatter"),
"response": response,
"files_list": files_list,
"file_path": os.path.join(settings.MEDIA_ROOT, file_path),
"file_path_getnames": file_path,
"file_name": file_name,
},
)
def post(self, request):
file_path = request.POST.get("file_path")
with open(file_path, "rb") as zip_file:
response = HttpResponse(zip_file.read(), content_type="application/zip")
response[
"Content-Disposition"
] = f"attachment; filename={os.path.basename(file_path)}"
return response

View file

@ -0,0 +1,116 @@
import os
from judge.views.test_formatter import test_formatter as tf
from judge.views.test_formatter import tf_pattern as pattern
class TestSuite:
def __init__(
self,
file_id: str,
pattern_pair: pattern.PatternPair,
test_id_list: list,
extra_files: list,
):
self.file_id = file_id
self.pattern_pair = pattern_pair
self.test_id_list = test_id_list
self.extra_files = extra_files
@classmethod
def get_test_suite(cls, file_name: str, inp_format: str, out_format: str):
pattern_pair = pattern.PatternPair.from_string_pair(inp_format, out_format)
names = tf.get_names_in_archive(file_name)
test_id_list, extra_files = pattern_pair.matches(
names, returns="test_id_with_extra_files"
)
return cls(file_name, pattern_pair, test_id_list, extra_files)
def get_name_list(self, add_extra_info=False):
important_files = []
for index, t in enumerate(self.test_id_list):
inp_name = self.pattern_pair.x.get_name(t, index=index, use_index=True)
out_name = self.pattern_pair.y.get_name(t, index=index, use_index=True)
important_files.extend([inp_name, out_name])
result = []
for name in important_files:
if add_extra_info:
result.append({"value": name, "is_extra_file": False})
else:
result.append(name)
for name in self.extra_files:
if add_extra_info:
result.append({"value": name, "is_extra_file": True})
else:
result.append(name)
return result
def is_valid_file_type(file_name):
_, ext = os.path.splitext(file_name)
return ext in [".zip", ".ZIP"]
def preview(params):
bif = params["bef_inp_format"]
bof = params["bef_out_format"]
aif = params["aft_inp_format"]
aof = params["aft_out_format"]
file_str = params["file_str"]
try:
test_suite = TestSuite.get_test_suite(file_str, bif, bof)
bef_preview = test_suite.get_name_list(add_extra_info=True)
try:
test_suite.pattern_pair = pattern.PatternPair.from_string_pair(aif, aof)
aft_preview = test_suite.get_name_list(add_extra_info=True)
return {"bef_preview": bef_preview, "aft_preview": aft_preview}
except:
return {"bef_preview": bef_preview, "aft_preview": []}
except:
test_suite = TestSuite.get_test_suite(file_id, "*", "*")
preview = test_suite.get_name_list(add_extra_info=True)
return {"bef_preview": preview, "aft_preview": []}
def convert(params):
bif = params["bef_inp_format"]
bof = params["bef_out_format"]
aif = params["aft_inp_format"]
aof = params["aft_out_format"]
file_str = params["file_str"]
file_name = params["file_name"]
file_path = params["file_path"]
test_suite = TestSuite.get_test_suite(file_str, bif, bof)
bef_preview = test_suite.get_name_list()
test_suite.pattern_pair = pattern.PatternPair.from_string_pair(aif, aof)
aft_preview = test_suite.get_name_list()
result = tf.get_renamed_archive(
file_str, file_name, file_path, bef_preview, aft_preview
)
return result
def prefill(params):
file_str = params["file_str"]
file_name = params["file_name"]
names = tf.get_names_in_archive(file_str)
pattern_pair = pattern.find_best_pattern_pair(names)
return {
"file_name": file_name,
"inp_format": pattern_pair.x.to_string(),
"out_format": pattern_pair.y.to_string(),
}
def preview_file(file_str):
names = tf.get_names_in_archive(file_str)
return names

View file

@ -0,0 +1,268 @@
import os
import random
from judge.views.test_formatter import tf_utils as utils
SAMPLE_SIZE = 16
NUMBERED_MM = ["0", "1", "00", "01", "000", "001", "0000", "0001"]
VALID_MM = ["*"] + NUMBERED_MM
MSG_TOO_MANY_OCCURRENCES = (
"400: Invalid pattern: Pattern cannot have more than one '{}'"
)
MSG_MM_NOT_FOUND = "400: Invalid pattern: Wildcard not found. Wildcard list: {}"
class Pattern:
def __init__(self, ll, mm, rr):
assert mm in VALID_MM, "Invalid wildcard"
self.ll = ll
self.mm = mm
self.rr = rr
def __repr__(self):
return "Pattern('{}', '{}', '{}')".format(self.ll, self.mm, self.rr)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
@classmethod
def from_string(cls, text):
for mm in ["*"] + sorted(NUMBERED_MM, key=len, reverse=True):
if mm in text:
if text.count(mm) > 1:
raise Exception(MSG_TOO_MANY_OCCURRENCES.format(mm))
i = text.index(mm)
return cls(text[:i], mm, text[i + len(mm) :])
raise Exception(MSG_MM_NOT_FOUND.format(",".join(VALID_MM)))
def to_string(self):
return self.ll + self.mm + self.rr
def is_valid_test_id(self, test_id):
if self.mm == "*":
return True
if self.mm in NUMBERED_MM:
return test_id.isdigit() and len(test_id) >= len(self.mm)
raise NotImplementedError
def matched(self, name):
return (
name.startswith(self.ll)
and name.endswith(self.rr)
and len(name) >= len(self.ll) + len(self.rr)
and self.is_valid_test_id(self.get_test_id(name))
)
def get_test_id(self, name):
return name[len(self.ll) : len(name) - len(self.rr)]
def get_test_id_from_index(self, index):
assert self.mm in NUMBERED_MM, "Wildcard is not a number"
return str(int(self.mm) + index).zfill(len(self.mm))
def get_name(self, test_id, index=None, use_index=False):
if use_index and self.mm in NUMBERED_MM:
return self.ll + self.get_test_id_from_index(index) + self.rr
return self.ll + test_id + self.rr
def matches(self, names, returns):
if returns == "test_id":
result = [n for n in names]
result = [n for n in result if self.matched(n)]
result = [self.get_test_id(n) for n in result]
return result
else:
raise NotImplementedError
class PatternPair:
def __init__(self, x: Pattern, y: Pattern):
assert x.mm == y.mm, "Input wildcard and output wildcard must be equal"
self.x = x
self.y = y
def __repr__(self):
return "PatternPair({}, {})".format(self.x, self.y)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
@classmethod
def from_string_pair(cls, inp_format, out_format):
return cls(Pattern.from_string(inp_format), Pattern.from_string(out_format))
def matches(self, names, returns):
x_test_ids = self.x.matches(names, returns="test_id")
y_test_ids = self.y.matches(names, returns="test_id")
test_ids = set(x_test_ids) & set(y_test_ids)
test_ids = list(sorted(test_ids, key=utils.natural_sorting_key))
if returns == "fast_count":
if self.x.mm == "*":
return len(test_ids)
elif self.x.mm in NUMBERED_MM:
count_valid = 0
for t in test_ids:
if t == self.x.get_test_id_from_index(count_valid):
count_valid += 1
return count_valid
extra_files = list(names)
valid_test_ids = []
for t in test_ids:
if self.x.mm in NUMBERED_MM:
if t != self.x.get_test_id_from_index(len(valid_test_ids)):
continue
inp_name = self.x.get_name(t)
out_name = self.y.get_name(t)
if inp_name == out_name:
continue
if inp_name not in extra_files:
continue
if out_name not in extra_files:
continue
valid_test_ids.append(t)
extra_files.remove(inp_name)
extra_files.remove(out_name)
if returns == "count":
return len(valid_test_ids)
elif returns == "test_id":
return valid_test_ids
elif returns == "test_id_with_extra_files":
return valid_test_ids, extra_files
else:
raise NotImplementedError
def score(self, names):
def ls(s):
return len(s) - s.count("0")
def zs(s):
return -s.count("0")
def vs(s):
return sum(
s.lower().count(c) * w
for c, w in [("a", -1), ("e", -1), ("i", +1), ("o", -1), ("u", -1)]
)
count_score = self.matches(names, returns="fast_count")
len_score = ls(self.x.ll + self.x.rr + self.y.ll + self.y.rr)
zero_score = zs(self.x.ll + self.x.rr + self.y.ll + self.y.rr)
assert self.x.mm in ["*"] + NUMBERED_MM
specific_score = 0 if self.x.mm == "*" else len(self.x.mm)
vowel_score = vs(self.x.ll + self.x.rr) - vs(self.y.ll + self.y.rr)
return count_score, specific_score, len_score, zero_score, vowel_score
def is_string_safe(self):
try:
x = Pattern.from_string(self.x.to_string())
y = Pattern.from_string(self.y.to_string())
return self == PatternPair(x, y)
except:
return False
def maximal(a, key):
max_score = max(map(key, a))
result = [x for x in a if key(x) == max_score]
if len(result) == 1:
return result[0]
else:
print(result)
raise Exception("More than one maximum values")
def get_all_star_pattern_pairs(names):
sample = random.sample(names, min(len(names), SAMPLE_SIZE))
star_pattern_pairs = []
all_prefixes = [n[:i] for n in sample for i in range(len(n) + 1)]
all_prefixes = list(sorted(set(all_prefixes)))
all_suffixes = [n[i:] for n in sample for i in range(len(n) + 1)]
all_suffixes = list(sorted(set(all_suffixes)))
for prefix in all_prefixes:
matched_names = [n for n in names if n.startswith(prefix)]
if len(matched_names) == 2:
mn0, mn1 = matched_names
for i in range(len(prefix) + 1):
x = Pattern(prefix[:i], "*", mn0[len(prefix) :])
y = Pattern(prefix[:i], "*", mn1[len(prefix) :])
star_pattern_pairs.append(PatternPair(x, y))
for suffix in all_suffixes:
matched_names = [n for n in names if n.endswith(suffix)]
if len(matched_names) == 2:
mn0, mn1 = matched_names
for i in range(len(suffix) + 1):
x = Pattern(mn0[: len(mn0) - len(suffix)], "*", suffix[i:])
y = Pattern(mn1[: len(mn1) - len(suffix)], "*", suffix[i:])
star_pattern_pairs.append(PatternPair(x, y))
star_pattern_pairs = list(set(star_pattern_pairs))
return star_pattern_pairs
def get_variant_pattern_pairs(pp):
return [
PatternPair(Pattern(pp.x.ll, mm, pp.x.rr), Pattern(pp.y.ll, mm, pp.y.rr))
for mm in VALID_MM
] + [
PatternPair(Pattern(pp.y.ll, mm, pp.y.rr), Pattern(pp.x.ll, mm, pp.x.rr))
for mm in VALID_MM
]
def find_best_pattern_pair(names):
star_pattern_pairs = get_all_star_pattern_pairs(names)
star_pattern_pairs = [
pp for pp in star_pattern_pairs if pp.matches(names, returns="fast_count") >= 2
]
# for pp in star_pattern_pairs:
# print(pp, pp.is_string_safe(), pp.score(names))
if len(star_pattern_pairs) == 0:
return PatternPair(Pattern("", "*", ""), Pattern("", "*", ""))
best_star_pattern_pair = maximal(star_pattern_pairs, key=lambda pp: pp.score(names))
pattern_pairs = get_variant_pattern_pairs(best_star_pattern_pair)
# for pp in pattern_pairs:
# print(pp, pp.is_string_safe(), pp.score(names))
pattern_pairs = [pp for pp in pattern_pairs if pp.is_string_safe()]
best_pattern_pair = maximal(pattern_pairs, key=lambda pp: pp.score(names))
return best_pattern_pair
def list_dir_recursively(folder):
old_cwd = os.getcwd()
os.chdir(folder)
result = []
for root, _, filenames in os.walk("."):
for filename in filenames:
result.append(os.path.join(root, filename))
os.chdir(old_cwd)
return result
def test_with_dir(folder):
names = list_dir_recursively(folder)
print(folder, find_best_pattern_pair(names))

View file

@ -0,0 +1,15 @@
def get_char_kind(char):
return 1 if char.isdigit() else 2 if char.isalpha() else 3
def natural_sorting_key(name):
result = []
last_kind = -1
for char in name:
curr_kind = get_char_kind(char)
if curr_kind != last_kind:
result.append("")
result[-1] += char
last_kind = curr_kind
return [x.zfill(16) if x.isdigit() else x for x in result]

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