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>
<div class="popup">
<div>
<img class="logo" src="logo.png" alt="LQDOJ">
<img class="logo" src="logo.svg" alt="LQDOJ">
</div>
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
</div>

View file

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

View file

@ -25,26 +25,18 @@ class Room(models.Model):
class Meta:
app_label = "chat_box"
@cache_wrapper(prefix="Rinfo")
def _info(self):
last_msg = self.message_set.first()
return {
"user_ids": [self.user_one.id, self.user_two.id],
"last_message": last_msg.body if last_msg else None,
}
@cached_property
def _cached_info(self):
return self._info()
return get_room_info(self.id)
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):
return self.user_one if profile == self.user_two else self.user_two
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
def users(self):
@ -53,6 +45,10 @@ class Room(models.Model):
def last_message_body(self):
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):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
@ -66,7 +62,6 @@ class Message(models.Model):
)
def save(self, *args, **kwargs):
new_message = self.id
self.body = self.body.strip()
super(Message, self).save(*args, **kwargs)
@ -148,3 +143,11 @@ class Ignore(models.Model):
self.remove_ignore(current_user, friend)
else:
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.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
@ -174,19 +174,46 @@ def mute_message(request):
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
def post_message(request):
ret = {"msg": "posted"}
if request.method != "POST":
return HttpResponseBadRequest()
if len(request.POST["body"]) > 5000:
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
return HttpResponseBadRequest()
room = None
if 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()
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
@ -204,7 +231,7 @@ def post_message(request):
},
)
else:
Room._info.dirty(room)
get_room_info.dirty(room.id)
room.last_msg_time = new_message.time
room.save()
@ -229,9 +256,7 @@ def post_message(request):
def can_access_room(request, room):
return (
not room or room.user_one == request.profile or room.user_two == request.profile
)
return not room or room.contain(request.profile)
@login_required
@ -247,7 +272,7 @@ def chat_message_ajax(request):
try:
message = Message.objects.filter(hidden=False).get(id=message_id)
room = message.room
if room and not room.contain(request.profile):
if not can_access_room(request, room):
return HttpResponse("Unauthorized", status=401)
except Message.DoesNotExist:
return HttpResponseBadRequest()
@ -278,7 +303,7 @@ def update_last_seen(request, **kwargs):
except Room.DoesNotExist:
return HttpResponseBadRequest()
if room and not room.contain(profile):
if not can_access_room(request, room):
return HttpResponseBadRequest()
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):
if not other_profile_ids:
return None
Profile.prefetch_profile_cache(other_profile_ids)
joined_ids = ",".join([str(id) for id in other_profile_ids])
other_profiles = Profile.objects.raw(
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_rooms = [int(i["id"]) for i in recent_profile]
Room.prefetch_room_cache(recent_rooms)
admin_list = (
queryset.filter(display_rank="admin")
@ -473,9 +501,16 @@ def get_or_create_room(request):
user_room.last_seen = timezone.now()
user_room.save()
room_url = reverse("chat", kwargs={"room_id": room.id})
if request.method == "GET":
return JsonResponse({"room": room.id, "other_user_id": other_user.id})
return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id}))
return JsonResponse(
{
"room": room.id,
"other_user_id": other_user.id,
"url": room_url,
}
)
return HttpResponseRedirect(room_url)
def get_unread_count(rooms, user):

View file

@ -34,6 +34,7 @@ SITE_ID = 1
SITE_NAME = "LQDOJ"
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
SITE_ADMIN_EMAIL = False
SITE_DOMAIN = "lqdoj.edu.vn"
DMOJ_REQUIRE_STAFF_2FA = True
@ -85,6 +86,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"ERR": "#ffa71c",
}
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {}
@ -130,13 +132,10 @@ USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
PYGMENT_THEME = "pygment-github.css"
INLINE_JQUERY = True
INLINE_FONTAWESOME = True
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
FONTAWESOME_CSS = (
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
)
FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
DMOJ_CANONICAL = ""
# Application definition
@ -170,7 +169,7 @@ else:
},
{
"model": "judge.Submission",
"icon": "fa-check-square-o",
"icon": "fa-check-square",
"children": [
"judge.Language",
"judge.Judge",
@ -278,8 +277,11 @@ LANGUAGE_COOKIE_AGE = 8640000
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE_REQUIRE_SUPERUSER = True
IMPERSONATE_DISABLE_LOGGING = True
IMPERSONATE = {
"REQUIRE_SUPERUSER": True,
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
ACCOUNT_ACTIVATION_DAYS = 7
@ -323,7 +325,6 @@ TEMPLATES = [
"judge.template_context.site",
"judge.template_context.site_name",
"judge.template_context.misc_config",
"judge.template_context.math_setting",
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
],
@ -431,7 +432,7 @@ AUTHENTICATION_BACKENDS = (
"social_core.backends.google.GoogleOAuth2",
"social_core.backends.facebook.FacebookOAuth2",
"judge.social_auth.GitHubSecureEmailOAuth2",
"django.contrib.auth.backends.ModelBackend",
"judge.authentication.CustomModelBackend",
)
SOCIAL_AUTH_PIPELINE = (
@ -488,6 +489,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
# Rate limit
RL_VOTE = "200/h"
RL_COMMENT = "30/h"
try:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
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 judge.feed import (
AtomBlogFeed,
AtomCommentFeed,
AtomProblemFeed,
BlogFeed,
CommentFeed,
ProblemFeed,
)
from judge.forms import CustomAuthenticationForm
from judge.sitemap import (
BlogPostSitemap,
@ -46,6 +38,7 @@ from judge.views import (
license,
mailgun,
markdown_editor,
test_formatter,
notification,
organization,
preview,
@ -68,7 +61,12 @@ from judge.views import (
resolver,
course,
email,
custom_file_upload,
)
from judge import authentication
from judge.views.test_formatter import test_formatter
from judge.views.problem_data import (
ProblemDataView,
ProblemSubmissionDiff,
@ -80,7 +78,6 @@ from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import (
AssigneeSelect2View,
ChatUserSearchSelect2View,
CommentSelect2View,
ContestSelect2View,
ContestUserSearchSelect2View,
OrganizationSelect2View,
@ -88,6 +85,7 @@ from judge.views.select2 import (
TicketUserSelect2View,
UserSearchSelect2View,
UserSelect2View,
ProblemAuthorSearchSelect2View,
)
admin.autodiscover()
@ -144,9 +142,7 @@ register_patterns = [
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
url(
r"^password/change/$",
auth_views.PasswordChangeView.as_view(
template_name="registration/password_change_form.html",
),
authentication.CustomPasswordChangeView.as_view(),
name="password_change",
),
url(
@ -403,7 +399,28 @@ urlpatterns = [
name="submission_status",
),
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])
),
),
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
url(
r"^/$",
lambda _, user: HttpResponsePermanentRedirect(
@ -519,11 +537,37 @@ urlpatterns = [
),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
url(
r"^contests/summary/(?P<key>\w+)$",
contests.contests_summary_view,
name="contests_summary",
r"^contests/summary/(?P<key>\w+)/",
paged_list_view(contests.ContestsSummaryView, "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(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
contests.ContestCalendar.as_view(),
@ -587,6 +631,13 @@ urlpatterns = [
"contest_user_submissions_ajax",
),
),
url(
r"^/submissions",
paged_list_view(
submission.ContestSubmissions,
"contest_submissions",
),
),
url(
r"^/participations$",
contests.ContestParticipationList.as_view(),
@ -852,6 +903,11 @@ urlpatterns = [
AssigneeSelect2View.as_view(),
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(
r"^stats/",
include(
@ -1023,9 +1066,6 @@ urlpatterns = [
url(
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
),
url(
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
),
]
),
),
@ -1131,8 +1171,7 @@ urlpatterns = [
),
url(
r"^notifications/",
login_required(notification.NotificationList.as_view()),
name="notification",
paged_list_view(notification.NotificationList, "notification"),
),
url(
r"^import_users/",
@ -1162,6 +1201,7 @@ urlpatterns = [
),
),
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)
# 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.runtime import JudgeAdmin, LanguageAdmin
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.volunteer import VolunteerProblemVoteAdmin
from judge.admin.course import CourseAdmin
from judge.models import (
BlogPost,
Comment,
@ -47,6 +53,8 @@ from judge.models import (
VolunteerProblemVote,
Course,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
)
@ -72,7 +80,9 @@ admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course)
admin.site.register(Course, CourseAdmin)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
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:
widgets = {
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
"parent": AdminHeavySelect2Widget(data_view="comment_select2"),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["body"] = HeavyPreviewAdminPageDownWidget(
@ -39,7 +38,7 @@ class CommentAdmin(VersionAdmin):
)
list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"]
readonly_fields = ["score"]
readonly_fields = ["score", "parent"]
actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"]
actions_on_top = True

View file

@ -14,7 +14,14 @@ from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin
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.widgets import (
AdminHeavySelect2MultipleWidget,
@ -24,6 +31,7 @@ from judge.widgets import (
AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.views.contests import recalculate_contest_summary_result
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):
fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
@ -162,6 +190,7 @@ class ContestAdmin(CompareVersionAdmin):
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"rate_limit",
)
},
),
@ -221,7 +250,7 @@ class ContestAdmin(CompareVersionAdmin):
"user_count",
)
search_fields = ("key", "name")
inlines = [ContestProblemInline]
inlines = [ContestProblemInline, OfficialContestInline]
actions_on_top = True
actions_on_bottom = True
form = ContestForm
@ -297,15 +326,23 @@ class ContestAdmin(CompareVersionAdmin):
self._rescore(obj.key)
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):
super().save_related(request, form, formsets, change)
# 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):
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):
if not request.user.has_perm("judge.edit_own_contest"):
@ -518,3 +555,9 @@ class ContestsSummaryAdmin(admin.ModelAdmin):
list_display = ("key",)
search_fields = ("key", "contests__key")
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,6 +53,7 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs):
super(BlogPostForm, self).__init__(*args, **kwargs)
if "authors" in self.fields:
self.fields["authors"].widget.can_add_related = False
class Meta:

View file

@ -1,8 +1,8 @@
from operator import attrgetter
from django import forms
from django.contrib import admin
from django.db import transaction
from django.contrib import admin, messages
from django.db import transaction, IntegrityError
from django.db.models import Q, Avg, Count
from django.db.models.aggregates import StdDev
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_ace import AceWidget
from django.utils import timezone
from django.core.exceptions import ValidationError
from reversion.admin import VersionAdmin
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):
memory_unit = self.cleaned_data.get("memory_unit", "KB")
if memory_unit == "MB":
@ -131,6 +142,7 @@ class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit
fields = ("language", "time_limit", "memory_limit", "memory_unit")
form = LanguageLimitInlineForm
extra = 0
class LanguageTemplateInlineForm(ModelForm):
@ -145,6 +157,7 @@ class LanguageTemplateInline(admin.TabularInline):
model = LanguageTemplate
fields = ("language", "source")
form = LanguageTemplateInlineForm
extra = 0
class ProblemSolutionForm(ModelForm):
@ -370,8 +383,6 @@ class ProblemAdmin(CompareVersionAdmin):
super().save_related(request, form, formsets, change)
obj = form.instance
obj.curators.add(request.profile)
obj.is_organization_private = obj.organizations.count() > 0
obj.save()
if "curators" in form.changed_data or "authors" in form.changed_data:
del obj.editor_ids

View file

@ -6,7 +6,7 @@ from reversion.admin import VersionAdmin
from django.contrib.auth.admin import UserAdmin as OldUserAdmin
from django_ace import AceWidget
from judge.models import Profile
from judge.models import Profile, ProfileInfo
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
@ -54,6 +54,13 @@ class TimezoneFilter(admin.SimpleListFilter):
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):
fields = (
"user",
@ -63,15 +70,12 @@ class ProfileAdmin(VersionAdmin):
"timezone",
"language",
"ace_theme",
"math_engine",
"last_access",
"ip",
"mute",
"is_unlisted",
"is_banned_problem_voting",
"notes",
"is_totp_enabled",
"user_script",
"current_contest",
)
readonly_fields = ("user",)
@ -92,6 +96,7 @@ class ProfileAdmin(VersionAdmin):
actions_on_top = True
actions_on_bottom = True
form = ProfileForm
inlines = (ProfileInfoInline,)
def get_queryset(self, request):
return super(ProfileAdmin, self).get_queryset(request).select_related("user")
@ -160,15 +165,6 @@ class ProfileAdmin(VersionAdmin):
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):
# 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 []
)
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.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# 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.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,
SubmissionTestCase,
)
from judge.bridge.utils import VanishedSubmission
from judge.caching import cache_wrapper
logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge")
@ -65,9 +67,8 @@ class JudgeHandler(ZlibPacketHandler):
self._working = False
self._working_data = {}
self._no_response_job = None
self._problems = []
self.executors = {}
self.problems = {}
self.problems = set()
self.latency = None
self.time_delta = None
self.load = 1e100
@ -139,11 +140,52 @@ class JudgeHandler(ZlibPacketHandler):
)
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):
judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now()
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())))
# 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):
Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete()
self.judge.problems.clear()
_get_judge_problems.dirty(self.judge)
def _update_ping(self):
try:
@ -208,8 +252,7 @@ class JudgeHandler(ZlibPacketHandler):
return
self.timeout = 60
self._problems = packet["problems"]
self.problems = dict(self._problems)
self._update_supported_problems(packet["problems"])
self.executors = packet["executors"]
self.name = packet["id"]
@ -310,6 +353,9 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source):
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_data = {
"problem": problem,
@ -434,14 +480,12 @@ class JudgeHandler(ZlibPacketHandler):
def on_supported_problems(self, packet):
logger.info("%s: Updated problem list", self.name)
self._problems = packet["problems"]
self.problems = dict(self._problems)
self._update_supported_problems(packet["problems"])
if not self.working:
self.judges.update_problems(self)
self.judge.problems.set(
Problem.objects.filter(code__in=list(self.problems.keys()))
)
self._update_judge_problems()
json_log.info(
self._make_json_log(action="update-problems", count=len(self.problems))
)
@ -658,8 +702,11 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet)
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(
status="IE", result="IE", error=packet["message"]
status="IE", result="IE", error=message
):
event.post(
"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)
json_log.info(
self._make_json_log(
packet,
sub=id,
action="internal-error",
message=packet["message"],
message=message,
finish=True,
result="IE",
)
@ -678,10 +725,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id)
json_log.error(
self._make_json_log(
packet,
sub=id,
action="internal-error",
info="unknown submission",
message=packet["message"],
message=message,
finish=True,
result="IE",
)
@ -912,3 +959,8 @@ class JudgeHandler(ZlibPacketHandler):
def on_cleanup(self):
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 threading import RLock
from judge.bridge.utils import VanishedSubmission
try:
from llist import dllist
except ImportError:
@ -39,6 +41,8 @@ class JudgeList(object):
)
try:
judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception:
logger.exception(
"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
from judge.logging import log_debug
MAX_NUM_CHAR = 50
NONE_RESULT = "__None__"
@ -26,7 +28,7 @@ def filter_args(args_list):
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):
args_list = list(args)
signature_args = list(signature(func).parameters.keys())
@ -40,7 +42,10 @@ def cache_wrapper(prefix, timeout=None):
def _get(key):
if not l0_cache:
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):
if l0_cache:
@ -51,18 +56,33 @@ def cache_wrapper(prefix, timeout=None):
cache.set(key, value, timeout)
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):
cache_key = get_key(func, *args, **kwargs)
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)
if result == NONE_RESULT:
if type(result) == str and result == NONE_RESULT:
result = None
return result
result = func(*args, **kwargs)
if result is None:
result = NONE_RESULT
_set(cache_key, result, timeout)
cache_result = NONE_RESULT
else:
cache_result = result
_set(cache_key, cache_result, timeout)
return result
def dirty(*args, **kwargs):
@ -71,7 +91,26 @@ def cache_wrapper(prefix, timeout=None):
if l0_cache:
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.prefetch_multi = prefetch_multi
wrapper.dirty_multi = dirty_multi
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:
problem = str(result["problem_id"])
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
continue
if format_data.get(problem):
is_after_freeze = (
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",
"language": 1,
"last_access": "2017-12-02T08:57:10.093Z",
"math_engine": "auto",
"mute": false,
"organizations": [
1
@ -18,8 +17,7 @@
"problem_count": 0,
"rating": null,
"timezone": "America/Toronto",
"user": 1,
"user_script": ""
"user": 1
},
"model": "judge.profile",
"pk": 1

View file

@ -29,6 +29,7 @@ from django_ace import AceWidget
from judge.models import (
Contest,
Language,
TestFormatterModel,
Organization,
PrivateMessage,
Problem,
@ -37,11 +38,12 @@ from judge.models import (
Submission,
BlogPost,
ContestProblem,
TestFormatterModel,
ProfileInfo,
)
from judge.widgets import (
HeavyPreviewPageDownWidget,
MathJaxPagedownWidget,
PagedownWidget,
Select2MultipleWidget,
Select2Widget,
@ -50,6 +52,7 @@ from judge.widgets import (
Select2MultipleWidget,
DateTimePickerWidget,
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 Meta:
model = Profile
@ -76,12 +90,10 @@ class ProfileForm(ModelForm):
"timezone",
"language",
"ace_theme",
"user_script",
"profile_image",
"css_background",
]
widgets = {
"user_script": AceWidget(theme="github"),
"timezone": Select2Widget(attrs={"style": "width:200px"}),
"language": Select2Widget(attrs={"style": "width:200px"}),
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
@ -89,11 +101,6 @@ class ProfileForm(ModelForm):
"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:
widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("profile_preview"),
@ -301,8 +308,8 @@ class EditOrganizationContestForm(ModelForm):
"hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"rate_limit",
"description",
"og_image",
"logo_override_image",
@ -412,13 +419,15 @@ class NewMessageForm(ModelForm):
fields = ["title", "content"]
widgets = {}
if PagedownWidget is not None:
widgets["content"] = MathJaxPagedownWidget()
widgets["content"] = PagedownWidget()
class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *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.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
@ -566,3 +575,9 @@ class ContestProblemFormSet(
)
):
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 judge.markdown import markdown
__all__ = ["highlight_code"]
def _make_pre_code(code):
return mark_safe("<pre>" + escape(code) + "</pre>")
def highlight_code(code, language, linenos=True, title=None):
linenos_option = 'linenums="1"' if linenos else ""
title_option = f'title="{title}"' if title else ""
options = f"{{.{language} {linenos_option} {title_option}}}"
try:
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),
)
)
value = f"```{options}\n{code}\n```\n"
return mark_safe(markdown(value))

View file

@ -22,6 +22,7 @@ from . import (
social,
spaceless,
timedelta,
comment,
)
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.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}

View file

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

View file

@ -1,112 +1,7 @@
from .. import registry
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"]
from judge.markdown import markdown as _markdown
@registry.filter
def markdown(value, lazy_load=False):
extensions = EXTENSIONS
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
return _markdown(value, lazy_load)

View file

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

View file

@ -48,5 +48,9 @@ for name, template, url_func in SHARES:
@registry.function
def recaptcha_init(language=None):
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
error_log = logging.getLogger("judge.errors")
debug_log = logging.getLogger("judge.debug")
def 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
else trans.description,
"url": "",
"math_engine": maker.math_engine,
}
)
.replace('"//', '"https://')
.replace("'//", "'https://")
)
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.make(debug=True)
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
from django.conf import settings
import os
import hashlib
from django.core.cache import cache
from django.conf import settings
from judge.caching import cache_wrapper
@ -12,67 +14,69 @@ class CollabFilter:
# name = 'collab_filter' or 'collab_filter_time'
def __init__(self, name):
embeddings = np.load(
self.embeddings = np.load(
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
allow_pickle=True,
)
arr0, arr1 = embeddings.files
_, problem_arr = self.embeddings.files
self.name = name
self.user_embeddings = embeddings[arr0]
self.problem_embeddings = embeddings[arr1]
self.problem_embeddings = self.embeddings[problem_arr].item()
def __str__(self):
return self.name
def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
"""Computes the scores of the candidates given a query.
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.
"""
"""Return {id: score}"""
u = query_embedding
V = item_embeddings
V = np.stack(list(item_embeddings.values()))
if measure == self.COSINE:
V = V / np.linalg.norm(V, axis=1, keepdims=True)
u = u / np.linalg.norm(u)
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)
def user_recommendations(self, user, problems, measure=DOT, limit=None):
uid = user.id
if uid >= len(self.user_embeddings):
uid = 0
scores = self.compute_scores(
self.user_embeddings[uid], self.problem_embeddings, measure
)
def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
user_embedding = self.get_user_embedding(user_id)
scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
res = [] # [(score, problem)]
for pid in problems:
# pid = problem.id
if pid < len(scores):
if pid in scores:
res.append((scores[pid], pid))
res.sort(reverse=True, key=lambda x: x[0])
res = res[:limit]
return res
return res[:limit]
# return a list of pid
def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
pid = problem.id
if pid >= len(self.problem_embeddings):
if pid not in self.problem_embeddings:
return []
scores = self.compute_scores(
self.problem_embeddings[pid], self.problem_embeddings, measure
)
embedding = self.problem_embeddings[pid]
scores = self.compute_scores(embedding, self.problem_embeddings, measure)
res = []
for p in problemset:
if p < len(scores):
if p in scores:
res.append((scores[p], p))
res.sort(reverse=True, key=lambda x: x[0])
return res[:limit]

View file

@ -2,8 +2,6 @@ from reversion import revisions
from judge.models.choices import (
ACE_THEMES,
EFFECTIVE_MATH_ENGINES,
MATH_ENGINES_CHOICES,
TIMEZONE,
)
from judge.models.comment import Comment, CommentLock, CommentVote
@ -17,6 +15,9 @@ from judge.models.contest import (
Rating,
ContestProblemClarification,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
OfficialContest,
)
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread
@ -45,6 +46,7 @@ from judge.models.profile import (
Profile,
Friend,
OrganizationProfile,
ProfileInfo,
)
from judge.models.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import (
@ -53,12 +55,15 @@ from judge.models.submission import (
SubmissionSource,
SubmissionTestCase,
)
from judge.models.test_formatter import TestFormatterModel
from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark
from judge.models.course import Course
from judge.models.course import Course, CourseRole, CourseLesson
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(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 judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["BookMark"]
@ -21,12 +22,9 @@ class BookMark(models.Model):
object_id = models.PositiveIntegerField()
linked_object = GenericForeignKey("content_type", "object_id")
def get_bookmark(self, user):
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user)
if userqueryset.exists():
return True
else:
return False
@cache_wrapper(prefix="BMgb")
def is_bookmarked_by(self, user):
return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
class Meta:
verbose_name = _("bookmark")
@ -55,11 +53,22 @@ class MakeBookMark(models.Model):
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:
def get_or_create_bookmark(self):
if self.bookmark.count():
return self.bookmark.first()
new_bookmark = BookMark()
new_bookmark.linked_object = self
new_bookmark.save()
return new_bookmark
content_type = ContentType.objects.get_for_model(self)
object_id = self.pk
return _get_or_create_bookmark(content_type, object_id)
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"),
("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.profile import Profile
from judge.utils.cachedict import CacheDict
from judge.caching import cache_wrapper
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
@ -56,6 +57,7 @@ class Comment(MPTTModel):
related_name="replies",
on_delete=CASCADE,
)
revision_count = models.PositiveIntegerField(default=1)
versions = VersionRelation()
@ -71,19 +73,14 @@ class Comment(MPTTModel):
@classmethod
def most_recent(cls, user, n, batch=None, organization=None):
queryset = (
cls.objects.filter(hidden=False)
.select_related("author__user")
.defer("author__about", "body")
.order_by("-id")
)
queryset = cls.objects.filter(hidden=False).order_by("-id")
if organization:
queryset = queryset.filter(author__in=organization.members.all())
problem_access = CacheDict(lambda p: p.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:
n = len(queryset)
@ -118,10 +115,6 @@ class Comment(MPTTModel):
query = Comment.filter(parent=self)
return len(query)
@cached_property
def get_revisions(self):
return self.versions.count()
@cached_property
def page_title(self):
if isinstance(self.linked_object, Problem):
@ -177,3 +170,10 @@ class CommentLock(models.Model):
def __str__(self):
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.db import models, transaction
from django.db.models import CASCADE, Q
from django.db.models.signals import m2m_changed
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.contenttypes.fields import GenericRelation
from django.dispatch import receiver
from jsonfield import JSONField
from lupa import LuaRuntime
from moss import (
@ -25,6 +28,7 @@ from judge.ratings import rate_contest
from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable
from judge.fulltext import SearchManager
from judge.caching import cache_wrapper
__all__ = [
"Contest",
@ -35,6 +39,9 @@ __all__ = [
"Rating",
"ContestProblemClarification",
"ContestsSummary",
"OfficialContest",
"OfficialContestCategory",
"OfficialContestLocation",
]
@ -310,6 +317,15 @@ class Contest(models.Model, PageVotable, Bookmarkable):
validators=[MinValueValidator(0), MaxValueValidator(10)],
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")
pagevote = GenericRelation("PageVote")
bookmark = GenericRelation("BookMark")
@ -446,28 +462,44 @@ class Contest(models.Model, PageVotable, Bookmarkable):
def ended(self):
return self.end_time < self._now
@cached_property
def author_ids(self):
return Contest.authors.through.objects.filter(contest=self).values_list(
@cache_wrapper(prefix="Coai")
def _author_ids(self):
return set(
Contest.authors.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
)
@cached_property
def editor_ids(self):
return self.author_ids.union(
@cache_wrapper(prefix="Coci")
def _curator_ids(self):
return set(
Contest.curators.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
)
@cached_property
def tester_ids(self):
return Contest.testers.through.objects.filter(contest=self).values_list(
@cache_wrapper(prefix="Coti")
def _tester_ids(self):
return set(
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):
return self.name
return f"{self.name} ({self.key})"
def get_absolute_url(self):
return reverse("contest_view", args=(self.key,))
@ -632,6 +664,20 @@ class Contest(models.Model, PageVotable, Bookmarkable):
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):
LIVE = 0
SPECTATE = -1
@ -920,6 +966,7 @@ class ContestsSummary(models.Model):
max_length=20,
unique=True,
)
results = models.JSONField(null=True, blank=True)
class Meta:
verbose_name = _("contests summary")
@ -930,3 +977,53 @@ class ContestsSummary(models.Model):
def get_absolute_url(self):
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.db import models
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
__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):
@ -20,10 +22,7 @@ class Course(models.Model):
max_length=128,
verbose_name=_("course name"),
)
about = models.TextField(verbose_name=_("organization description"))
ending_time = models.DateTimeField(
verbose_name=_("ending time"),
)
about = models.TextField(verbose_name=_("course description"))
is_public = models.BooleanField(
verbose_name=_("publicly visible"),
default=False,
@ -57,35 +56,50 @@ class Course(models.Model):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("course_detail", args=(self.slug,))
@classmethod
def is_editable_by(course, profile):
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
def is_editable_by(cls, course, profile):
try:
course_role = CourseRole.objects.get(course=course, user=profile)
return course_role.role in EDITABLE_ROLES
except CourseRole.DoesNotExist:
return False
@classmethod
def is_accessible_by(cls, course, profile):
userqueryset = CourseRole.objects.filter(course=course, user=profile)
if userqueryset.exists():
if not profile:
return False
try:
course_role = CourseRole.objects.get(course=course, user=profile)
if course_role.course.is_public:
return True
else:
return course_role.role in EDITABLE_ROLES
except CourseRole.DoesNotExist:
return False
@classmethod
def get_students(cls, course):
return CourseRole.objects.filter(course=course, role="ST").values("user")
def get_accessible_courses(cls, profile):
return Course.objects.filter(
Q(is_public=True) | Q(courserole__role__in=EDITABLE_ROLES),
courserole__user=profile,
).distinct()
@classmethod
def get_assistants(cls, course):
return CourseRole.objects.filter(course=course, role="AS").values("user")
def _get_users_by_role(self, role):
course_roles = CourseRole.objects.filter(course=self, role=role).select_related(
"user"
)
return [course_role.user for course_role in course_roles]
@classmethod
def get_teachers(cls, course):
return CourseRole.objects.filter(course=course, role="TE").values("user")
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
def add_student(cls, course, profiles):
@ -104,7 +118,7 @@ class Course(models.Model):
class CourseRole(models.Model):
course = models.OneToOneField(
course = models.ForeignKey(
Course,
verbose_name=_("course"),
on_delete=models.CASCADE,
@ -114,14 +128,9 @@ class CourseRole(models.Model):
Profile,
verbose_name=_("user"),
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(
max_length=2,
choices=RoleInCourse.choices,
@ -140,44 +149,19 @@ class CourseRole(models.Model):
couresrole.role = role
couresrole.save()
class Meta:
unique_together = ("course", "user")
class CourseResource(models.Model):
course = models.OneToOneField(
class CourseLesson(models.Model):
course = models.ForeignKey(
Course,
verbose_name=_("course"),
on_delete=models.CASCADE,
db_index=True,
)
files = models.FileField(
verbose_name=_("course files"),
null=True,
blank=True,
upload_to=course_directory_file,
)
description = models.CharField(
verbose_name=_("description"),
blank=True,
max_length=150,
)
order = models.IntegerField(null=True, default=None)
is_public = models.BooleanField(
verbose_name=_("publicly visible"),
default=False,
)
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"),
related_name="lessons",
on_delete=models.CASCADE,
)
points = models.FloatField(
verbose_name=_("points"),
)
title = models.TextField(verbose_name=_("course title"))
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.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable
from judge.caching import cache_wrapper
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
@ -105,7 +106,7 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
def get_absolute_url(self):
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 not self.is_organization_private:
return True
@ -132,6 +133,10 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
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:
permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post")

View file

@ -31,11 +31,8 @@ class PageVote(models.Model):
@cache_wrapper(prefix="PVvs")
def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
if page_vote.exists():
return page_vote.first().score
else:
return 0
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user).first()
return page_vote.score if page_vote else 0
def __str__(self):
return f"pagevote for {self.linked_object}"
@ -52,11 +49,22 @@ class PageVoteVoter(models.Model):
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:
def get_or_create_pagevote(self):
if self.pagevote.count():
return self.pagevote.first()
new_pagevote = PageVote()
new_pagevote.linked_object = self
new_pagevote.save()
return new_pagevote
content_type = ContentType.objects.get_for_model(self)
object_id = self.pk
return _get_or_create_pagevote(content_type, object_id)
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.utils.functional import cached_property
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.models.pagevote import PageVotable
@ -22,6 +24,7 @@ from judge.models.problem_data import (
problem_data_storage,
problem_directory_file_helper,
)
from judge.caching import cache_wrapper
__all__ = [
"ProblemGroup",
@ -437,6 +440,10 @@ class Problem(models.Model, PageVotable, Bookmarkable):
"profile_id", flat=True
)
@cache_wrapper(prefix="Pga", expected_type=models.query.QuerySet)
def get_authors(self):
return self.authors.only("id")
@cached_property
def editor_ids(self):
return self.author_ids.union(
@ -554,22 +561,37 @@ class Problem(models.Model, PageVotable, Bookmarkable):
cache.set(key, result)
return result
def save(self, *args, **kwargs):
super(Problem, self).save(*args, **kwargs)
if self.__original_code and self.code != self.__original_code:
if hasattr(self, "data_files") or self.pdf_description:
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 self.pdf_description:
if has_pdf:
self.pdf_description.name = problem_directory_file_helper(
self.code, self.pdf_description.name
)
if hasattr(self, "data_files"):
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)
if code_changed and should_move_data:
self.handle_code_change()
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
problem_data_storage.delete_directory(self.code)
save.alters_data = True
class Meta:
@ -682,6 +704,10 @@ class Solution(models.Model, PageVotable, Bookmarkable):
else:
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):
return _("Editorial for %s") % self.problem.name
@ -719,3 +745,10 @@ class ProblemPointsVote(models.Model):
def __str__(self):
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")),
("linecount", _("Line-by-line")),
("custom", _("Custom checker (PY)")),
("customval", _("Custom validator (CPP)")),
("customcpp", _("Custom checker (CPP)")),
("interact", _("Interactive")),
("testlib", _("Testlib")),
)
@ -90,8 +90,8 @@ class ProblemData(models.Model):
upload_to=problem_directory_file,
validators=[FileExtensionValidator(allowed_extensions=["py"])],
)
custom_validator = models.FileField(
verbose_name=_("custom validator file"),
custom_checker_cpp = models.FileField(
verbose_name=_("custom cpp checker file"),
storage=problem_data_storage,
null=True,
blank=True,
@ -186,9 +186,9 @@ class ProblemData(models.Model):
self.custom_checker.name = problem_directory_file_helper(
new, self.custom_checker.name
)
if self.custom_validator:
self.custom_validator.name = problem_directory_file_helper(
new, self.custom_validator.name
if self.custom_checker_cpp:
self.custom_checker_cpp.name = problem_directory_file_helper(
new, self.custom_checker_cpp.name
)
if self.interactive_judge:
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 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.ratings import rating_class
from judge.caching import cache_wrapper
@ -26,6 +26,15 @@ from judge.caching import cache_wrapper
__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):
def get_prep_value(self, value):
if not value:
@ -55,7 +64,9 @@ class Organization(models.Model):
verbose_name=_("short name"),
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(
"Profile",
verbose_name=_("registrant"),
@ -139,6 +150,14 @@ class Organization(models.Model):
def get_submissions_url(self):
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:
ordering = ["name"]
permissions = (
@ -154,7 +173,9 @@ class Profile(models.Model):
user = models.OneToOneField(
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(
max_length=50,
verbose_name=_("location"),
@ -201,19 +222,7 @@ class Profile(models.Model):
help_text=_("User will not be ranked."),
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)
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(
"ContestParticipation",
verbose_name=_("current contest"),
@ -222,13 +231,6 @@ class Profile(models.Model):
related_name="+",
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(
verbose_name=_("2FA enabled"),
default=False,
@ -260,23 +262,9 @@ class Profile(models.Model):
max_length=300,
)
@cache_wrapper(prefix="Pgbi2")
def _get_basic_info(self):
profile_image_url = None
if self.profile_image:
profile_image_url = self.profile_image.url
return {
"first_name": self.user.first_name,
"last_name": self.user.last_name,
"email": self.user.email,
"username": self.user.username,
"mute": self.mute,
"profile_image_url": profile_image_url,
}
@cached_property
def _cached_info(self):
return self._get_basic_info()
return _get_basic_info(self.id)
@cached_property
def organization(self):
@ -290,11 +278,11 @@ class Profile(models.Model):
@cached_property
def first_name(self):
return self._cached_info["first_name"]
return self._cached_info.get("first_name", "")
@cached_property
def last_name(self):
return self._cached_info["last_name"]
return self._cached_info.get("last_name", "")
@cached_property
def email(self):
@ -304,9 +292,17 @@ class Profile(models.Model):
def is_muted(self):
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
def profile_image_url(self):
return self._cached_info["profile_image_url"]
return self._cached_info.get("profile_image_url")
@cached_property
def count_unseen_notifications(self):
@ -398,7 +394,7 @@ class Profile(models.Model):
@cached_property
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
friend_obj = self.following_users.prefetch_related("users")
@ -412,13 +408,16 @@ class Profile(models.Model):
if not self.user.is_authenticated:
return False
profile_id = self.id
return (
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id
or self.user.is_superuser
)
return org.is_admin(self) or self.user.is_superuser
@classmethod
def prefetch_profile_cache(self, profile_ids):
_get_basic_info.prefetch_multi([(pid,) for pid in profile_ids])
class Meta:
indexes = [
models.Index(fields=["is_unlisted", "performance_points"]),
]
permissions = (
("test_site", "Shows in-progress development stuff"),
("totp", "Edit TOTP settings"),
@ -427,6 +426,36 @@ class Profile(models.Model):
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):
user = models.ForeignKey(
Profile,
@ -468,11 +497,7 @@ class Friend(models.Model):
@classmethod
def is_friend(self, current_user, new_friend):
try:
return (
current_user.following_users.get()
.users.filter(user=new_friend.user)
.exists()
)
return current_user.following_users.filter(users=new_friend).exists()
except:
return False
@ -506,7 +531,7 @@ class Friend(models.Model):
class OrganizationProfile(models.Model):
users = models.ForeignKey(
profile = models.ForeignKey(
Profile,
verbose_name=_("user"),
related_name="last_visit",
@ -525,37 +550,66 @@ class OrganizationProfile(models.Model):
)
@classmethod
def remove_organization(self, users, organization):
organizationprofile = self.objects.filter(
users=users, organization=organization
def remove_organization(self, profile, organization):
organization_profile = self.objects.filter(
profile=profile, organization=organization
)
if organizationprofile.exists():
organizationprofile.delete()
if organization_profile.exists():
organization_profile.delete()
@classmethod
def add_organization(self, users, organization):
self.remove_organization(users, organization)
new_organization = OrganizationProfile(users=users, organization=organization)
new_organization.save()
def add_organization(self, profile, organization):
self.remove_organization(profile, organization)
new_row = OrganizationProfile(profile=profile, organization=organization)
new_row.save()
@classmethod
def get_most_recent_organizations(self, users):
return self.objects.filter(users=users).order_by("-last_visit")[:5]
def get_most_recent_organizations(cls, profile):
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)
def on_user_save(sender, instance, **kwargs):
try:
profile = instance.profile
profile._get_basic_info.dirty(profile)
_get_basic_info.dirty(profile.id)
except:
pass
@receiver([pre_save], sender=Profile)
def on_profile_save(sender, instance, **kwargs):
if instance.id is None:
return
prev = sender.objects.get(id=instance.id)
if prev.mute != instance.mute or prev.profile_image != instance.profile_image:
instance._get_basic_info.dirty(instance)
@cache_wrapper(prefix="Pgbi3", expected_type=dict)
def _get_basic_info(profile_id):
profile = (
Profile.objects.select_related("user")
.only(
"id",
"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 judge.judgeapi import disconnect_judge
from judge.caching import cache_wrapper
__all__ = ["Language", "RuntimeVersion", "Judge"]
@ -147,14 +148,11 @@ class Language(models.Model):
@classmethod
def get_default_language(cls):
try:
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
except Language.DoesNotExist:
return cls.get_python3()
return _get_default_language()
@classmethod
def get_default_language_pk(cls):
return cls.get_default_language().pk
return _get_default_language().pk
class Meta:
ordering = ["key"]
@ -162,6 +160,14 @@ class Language(models.Model):
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):
language = models.ForeignKey(
Language,

View file

@ -220,13 +220,7 @@ class Submission(models.Model):
def id_secret(self):
return self.get_id_secret(self.id)
def is_accessible_by(self, profile):
from judge.utils.problems import (
user_completed_ids,
user_tester_ids,
user_editable_ids,
)
def is_accessible_by(self, profile, check_contest=True):
if not profile:
return False
@ -236,15 +230,6 @@ class Submission(models.Model):
if profile.id == self.user_id:
return True
if problem_id in user_editable_ids(profile):
return True
if self.problem_id in user_completed_ids(profile):
if self.problem.is_public:
return True
if problem_id in user_tester_ids(profile):
return True
if user.has_perm("judge.change_submission"):
return True
@ -254,10 +239,26 @@ class Submission(models.Model):
if self.problem.is_public and user.has_perm("judge.view_public_submission"):
return True
if check_contest:
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
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
class Meta:
@ -276,6 +277,7 @@ class Submission(models.Model):
indexes = [
models.Index(fields=["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.utils import timezone
BETA2 = 328.33**2
RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
MEAN_INIT = 1400.0
@ -146,6 +145,8 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p):
def rate_contest(contest):
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_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 = [
"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.dispatch import receiver
import judge
from judge.utils.problems import finished_submission
from .models import (
BlogPost,
Comment,
Contest,
ContestSubmission,
EFFECTIVE_MATH_ENGINES,
Judge,
Language,
License,
@ -23,6 +23,8 @@ from .models import (
Problem,
Profile,
Submission,
NavigationBar,
Solution,
)
@ -46,21 +48,13 @@ def problem_update(sender, instance, **kwargs):
cache.delete_many(
[
make_template_fragment_key("submission_problem", (instance.id,)),
make_template_fragment_key("problem_feed", (instance.id,)),
"problem_tls:%s" % instance.id,
"problem_mls:%s" % instance.id,
]
)
cache.delete_many(
[
make_template_fragment_key("problem_html", (instance.id, engine, lang))
for lang, _ in settings.LANGUAGES
for engine in EFFECTIVE_MATH_ENGINES
]
)
cache.delete_many(
[
make_template_fragment_key("problem_authors", (instance.id, lang))
make_template_fragment_key("problem_html", (instance.id, lang))
for lang, _ in settings.LANGUAGES
]
)
@ -70,6 +64,7 @@ def problem_update(sender, instance, **kwargs):
for lang, _ in settings.LANGUAGES
]
)
Problem.get_authors.dirty(instance)
for lang, _ in settings.LANGUAGES:
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)
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"):
return
cache.delete_many(
[
make_template_fragment_key("user_about", (instance.id, engine))
for engine in EFFECTIVE_MATH_ENGINES
]
[make_template_fragment_key("user_about", (instance.id,))]
+ [
make_template_fragment_key("org_member_count", (org_id,))
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)
def contest_update(sender, instance, **kwargs):
@ -99,10 +95,7 @@ def contest_update(sender, instance, **kwargs):
cache.delete_many(
["generated-meta-contest:%d" % instance.id]
+ [
make_template_fragment_key("contest_html", (instance.id, engine))
for engine in EFFECTIVE_MATH_ENGINES
]
+ [make_template_fragment_key("contest_html", (instance.id,))]
)
@ -130,19 +123,8 @@ def comment_update(sender, instance, **kwargs):
@receiver(post_save, sender=BlogPost)
def post_update(sender, instance, **kwargs):
cache.delete_many(
[
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
]
)
cache.delete(make_template_fragment_key("post_content", (instance.id,)))
BlogPost.get_authors.dirty(instance)
@receiver(post_delete, sender=Submission)
@ -159,12 +141,9 @@ def contest_submission_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Organization)
def organization_update(sender, instance, **kwargs):
cache.delete_many(
[
make_template_fragment_key("organization_html", (instance.id, engine))
for engine in EFFECTIVE_MATH_ENGINES
]
)
cache.delete_many([make_template_fragment_key("organization_html", (instance.id,))])
for admin in instance.admins.all():
Organization.is_admin.dirty(instance, admin)
_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(
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.shortcuts import render
from django.urls import reverse
from django.utils.translation import gettext as _
from requests import HTTPError
from reversion import revisions
from social_core.backends.github import GithubOAuth2
@ -65,13 +66,13 @@ class UsernameForm(forms.Form):
max_length=30,
label="Username",
error_messages={
"invalid": "A username must contain letters, numbers, or underscores"
"invalid": _("A username must contain letters, numbers, or underscores")
},
)
def clean_username(self):
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"]
@ -89,7 +90,7 @@ def choose_username(backend, user, username=None, *args, **kwargs):
request,
"registration/username_select.html",
{
"title": "Choose a username",
"title": _("Choose a username"),
"form": form,
},
)
@ -118,7 +119,7 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs):
backend.strategy.request,
"registration/profile_creation.html",
{
"title": "Create your profile",
"title": _("Create your profile"),
"form": form,
},
)

View file

@ -8,7 +8,7 @@ from judge.utils.celery import Progress
__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:
start, end = id_range
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)
if results:
queryset = queryset.filter(result__in=results)
if contest:
queryset = queryset.filter(contest_object=contest)
if contests:
queryset = queryset.filter(contest_object__in=contests)
queryset = queryset.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS)
return queryset

View file

@ -1,3 +1,4 @@
import re
from functools import partial
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.utils.functional import SimpleLazyObject, new_method_proxy
from mptt.querysets import TreeQuerySet
from .models import MiscConfig, NavigationBar, Profile
from judge.caching import cache_wrapper
class FixedSimpleLazyObject(SimpleLazyObject):
@ -24,7 +28,6 @@ def get_resource(request):
scheme = "http"
return {
"PYGMENT_THEME": settings.PYGMENT_THEME,
"INLINE_JQUERY": settings.INLINE_JQUERY,
"INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME,
"JQUERY_JS": settings.JQUERY_JS,
@ -51,22 +54,28 @@ def comet_location(request):
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):
result = list(
NavigationBar.objects.extra(where=["%s REGEXP BINARY regex"], params=[path])[:1]
)
return (
result[0].get_ancestors(include_self=True).values_list("key", flat=True)
if result
else []
)
nav_bar_list = list(_nav_bar())
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)
if result:
while result.parent_id:
result = nav_bar_dict.get(result.parent_id)
return result.key
else:
return []
def general_info(request):
path = request.get_full_path()
return {
"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,
"perms": PermWrapper(request.user),
}
@ -119,13 +128,3 @@ def site_name(request):
"SITE_LONG_NAME": settings.SITE_LONG_NAME,
"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.conf import settings
from django.core.cache import cache
from judge.models import Profile
@ -15,11 +16,13 @@ class LogUserAccessMiddleware(object):
hasattr(request, "user")
and request.user.is_authenticated
and not getattr(request, "no_profile_update", False)
and not cache.get(f"user_log_update_{request.user.id}")
):
updates = {"last_access": now()}
# 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):
updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY)
Profile.objects.filter(user_id=request.user.pk).update(**updates)
cache.set(f"user_log_update_{request.user.id}", True, 120)
return response

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
from collections import defaultdict
from math import e
from datetime import datetime
from datetime import datetime, timedelta
import random
from enum import Enum
from django.conf import settings
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.utils import timezone
from django.utils.translation import gettext as _, gettext_noop
from django.http import Http404
from judge.models import Problem, Submission
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.
# The caller, SubmissionList.get_result_data will run ugettext on the name.
{"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",
"name": gettext_noop("Compile Error"),
"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",
"name": gettext_noop("Error"),
@ -165,7 +175,7 @@ def editable_problems(user, profile=None):
return subquery
@cache_wrapper(prefix="hp", timeout=900)
@cache_wrapper(prefix="hp", timeout=14400)
def hot_problems(duration, limit):
qs = Problem.get_public_problems().filter(
submission__date__gt=timezone.now() - duration
@ -222,7 +232,7 @@ def hot_problems(duration, limit):
return qs
@cache_wrapper(prefix="grp", timeout=26400)
@cache_wrapper(prefix="grp", timeout=14400)
def get_related_problems(profile, problem, limit=8):
if not profile or not settings.ML_OUTPUT_PATH:
return None
@ -248,3 +258,72 @@ def finished_submission(sub):
keys += ["contest_complete:%d" % participation.id]
keys += ["contest_attempted:%d" % participation.id]
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.views.generic import ListView
from judge.comments import CommentedDetailView
from judge.views.comment import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView
from judge.models import (
@ -23,9 +23,9 @@ from judge.models import (
from judge.models.profile import Organization, OrganizationProfile
from judge.utils.cachedict import CacheDict
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.views import TitleMixin
from judge.utils.users import get_rating_rank, get_points_rank, get_awards
from judge.views.feed import FeedView
@ -70,12 +70,37 @@ class HomeFeedView(FeedView):
profile_queryset = Profile.objects
if self.request.organization:
profile_queryset = self.request.organization.members
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by(
"-rating"
)[:10]
context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by(
"-performance_points"
)[:10]
context["top_rated"] = (
profile_queryset.filter(is_unlisted=False)
.order_by("-rating")
.only("id", "rating")[: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
@ -91,7 +116,7 @@ class PostList(HomeFeedView):
queryset = (
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
.order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations")
.prefetch_related("organizations")
)
filter = Q(is_organization_private=False)
if self.request.user.is_authenticated:
@ -126,7 +151,6 @@ class TicketFeed(HomeFeedView):
)
.order_by("-id")
.prefetch_related("linked_item")
.select_related("user__user")
)
else:
return []
@ -137,7 +161,6 @@ class TicketFeed(HomeFeedView):
Ticket.objects.order_by("-id")
.filter(is_open=True)
.prefetch_related("linked_item")
.select_related("user__user")
)
return filter_visible_tickets(tickets, self.request.user, profile)
else:
@ -180,25 +203,24 @@ class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDeta
def get_context_data(self, **kwargs):
context = super(PostView, self).get_context_data(**kwargs)
context["og_image"] = self.object.og_image
context["valid_user_to_show_edit"] = False
context["valid_org_to_show_edit"] = []
context["editable_orgs"] = []
if self.request.profile in self.object.authors.all():
context["valid_user_to_show_edit"] = True
orgs = list(self.object.organizations.all())
for valid_org_to_show_edit in self.object.organizations.all():
if self.request.profile in valid_org_to_show_edit.admins.all():
context["valid_user_to_show_edit"] = True
if context["valid_user_to_show_edit"]:
for post_org in self.object.organizations.all():
if post_org in self.request.profile.organizations.all():
context["valid_org_to_show_edit"].append(post_org)
if self.request.profile:
if self.request.profile.id in self.object.get_authors():
for org in orgs:
if org.is_member(self.request.profile):
context["editable_orgs"].append(org)
else:
for org in orgs:
if org.is_admin(self.request.profile):
context["editable_orgs"].append(org)
return context
def get_object(self, queryset=None):
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()
return post

View file

@ -8,13 +8,12 @@ from django.http import (
HttpResponseForbidden,
)
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.detail import SingleObjectMixin
from judge.dblock import LockModel
from django.views.generic import View, ListView
from judge.models.bookmark import BookMark, MakeBookMark, dirty_bookmark
__all__ = [
"dobookmark_page",
@ -33,30 +32,31 @@ def bookmark_page(request, delta):
try:
bookmark_id = int(request.POST["id"])
bookmark_page = BookMark.objects.filter(id=bookmark_id)
bookmark = BookMark.objects.get(id=bookmark_id)
except ValueError:
return HttpResponseBadRequest()
else:
if not bookmark_page.exists():
except BookMark.DoesNotExist:
raise Http404()
if delta == 0:
bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile
bookmark=bookmark, user=request.profile
)
if not bookmarklist.exists():
newbookmark = MakeBookMark(
bookmark=bookmark_page.first(),
bookmark=bookmark,
user=request.profile,
)
newbookmark.save()
else:
bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile
bookmark=bookmark, user=request.profile
)
if bookmarklist.exists():
bookmarklist.delete()
dirty_bookmark(bookmark, request.profile)
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.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.context_processors import PermWrapper
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError, transaction
from django.db.models import Q, F, Count, FilteredRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError
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.expressions import F, Value
from django.forms.models import ModelForm
from django.forms import ModelForm
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
)
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.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.models import Version
from reversion.models import Revision, Version
from judge.dblock import LockModel
from judge.models import Comment, CommentVote, Notification, BlogPost
from judge.jinja2.reference import get_user_from_text
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.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget
from judge.comments import add_mention_notifications
import json
from judge.widgets import HeavyPreviewPageDownWidget
__all__ = [
"upvote_comment",
@ -39,7 +50,20 @@ __all__ = [
"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
def vote_comment(request, delta):
if abs(delta) != 1:
@ -77,18 +101,13 @@ def vote_comment(request, delta):
vote.voter = request.profile
vote.score = delta
while True:
try:
vote.save()
except IntegrityError:
with LockModel(write=(CommentVote,)):
try:
vote = CommentVote.objects.get(
comment_id=comment_id, voter=request.profile
)
vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile)
except CommentVote.DoesNotExist:
# We must continue racing in case this is exploited to manipulate votes.
continue
raise Http404()
if -vote.score != delta:
return HttpResponseBadRequest(
_("You already voted."), content_type="text/plain"
@ -97,7 +116,6 @@ def vote_comment(request, delta):
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")
@ -113,7 +131,7 @@ def get_comments(request, limit=10):
try:
comment_id = int(request.GET["id"])
parent_none = int(request.GET["parent_none"])
except ValueError:
except (ValueError, MultiValueDictKeyError):
return HttpResponseBadRequest()
else:
if comment_id and not Comment.objects.filter(id=comment_id).exists():
@ -121,7 +139,10 @@ def get_comments(request, limit=10):
offset = 0
if "offset" in request.GET:
try:
offset = int(request.GET["offset"])
except ValueError:
return HttpResponseBadRequest()
target_comment = -1
if "target_comment" in request.GET:
@ -147,7 +168,6 @@ def get_comments(request, limit=10):
.defer("author__about")
.annotate(
count_replies=Count("replies", distinct=True),
revisions=Count("versions", distinct=True),
)[offset : offset + limit]
)
profile = None
@ -241,8 +261,9 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
# update notifications
comment = form.instance
add_mention_notifications(comment)
with transaction.atomic(), revisions.create_revision():
comment.revision_count = comment.versions.count() + 1
comment.save(update_fields=["revision_count"])
with revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
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_descendants(include_self=True).update(hidden=True)
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
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,
When,
)
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db.models.expressions import CombinedExpression
from django.http import (
@ -56,7 +55,7 @@ from django.views.generic.detail import (
)
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.models import (
Contest,
@ -70,6 +69,8 @@ from judge.models import (
Submission,
ContestProblemClarification,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
)
from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status
@ -107,6 +108,7 @@ __all__ = [
"base_contest_ranking_list",
"ContestClarificationView",
"update_contest_mode",
"OfficialContestList",
]
@ -130,8 +132,17 @@ def _find_contest(request, key):
class ContestListMixin(object):
official = False
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(
@ -141,119 +152,190 @@ class ContestList(
paginate_by = 10
template_name = "contest/list.html"
title = gettext_lazy("Contests")
context_object_name = "past_contests"
all_sorts = frozenset(("name", "user_count", "start_time"))
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
def _now(self):
return timezone.now()
def get(self, request, *args, **kwargs):
self.contest_query = None
self.org_query = []
self.show_orgs = 0
if request.GET.get("show_orgs"):
self.show_orgs = 1
def GET_with_session(self, request, key):
if not request.GET.get(key):
return request.session.get(key, False)
return request.GET.get(key, None) == "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:
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 = [
i
for i in self.org_query
if i
in self.request.profile.organizations.values_list(
"id", flat=True
in set(
request.profile.organizations.values_list("id", flat=True)
)
]
except ValueError:
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)
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):
queryset = (
super(ContestList, self)
.get_queryset()
.prefetch_related("tags", "organizations", "authors", "curators", "testers")
.prefetch_related("tags", "organizations")
)
if "contest" in self.request.GET:
self.contest_query = query = " ".join(
self.request.GET.getlist("contest")
).strip()
if query:
if self.contest_query:
substr_queryset = queryset.filter(
Q(key__icontains=query) | Q(name__icontains=query)
Q(key__icontains=self.contest_query)
| Q(name__icontains=self.contest_query)
)
if settings.ENABLE_FTS:
queryset = (
queryset.search(query).extra(order_by=["-relevance"])
queryset.search(self.contest_query).extra(order_by=["-relevance"])
| substr_queryset
)
else:
queryset = substr_queryset
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
if self.show_orgs:
if self.hide_organization_contests:
queryset = queryset.filter(organizations=None)
if self.org_query:
queryset = queryset.filter(organizations__in=self.org_query)
queryset = self.extra_queryset_filters(queryset)
return queryset
def get_queryset(self):
def _get_past_contests_queryset(self):
return (
self._get_queryset()
.order_by(self.order, "key")
.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):
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:
for participation in (
ContestParticipation.objects.filter(
virtual=0, user=self.request.profile, contest_id__in=present
)
.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)
context["current_tab"] = self.current_tab
context["current_count"] = self._get_current_contests_queryset().count()
context["future_count"] = self._get_future_contests_queryset().count()
context["active_count"] = len(self._get_active_participations_queryset())
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["first_page_href"] = "."
context["contest_query"] = self.contest_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.user.is_superuser:
context["organizations"] = Organization.objects.all()
else:
context["organizations"] = self.request.profile.organizations.all()
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_paginate_context())
return context
@ -346,6 +428,19 @@ class ContestMixin(object):
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):
contest = super(ContestMixin, self).get_object(queryset)
profile = self.request.profile
@ -361,18 +456,8 @@ class ContestMixin(object):
if self.should_bypass_access_check(contest):
return 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()
else:
self.contest_access_check(contest)
return contest
def dispatch(self, request, *args, **kwargs):
@ -449,6 +534,10 @@ class ContestDetail(
)
context["editable_organizations"] = self.get_editable_organizations()
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
@ -459,7 +548,12 @@ def is_contest_clonable(request, contest):
return False
if request.user.has_perm("judge.clone_contest"):
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 False
@ -498,6 +592,7 @@ class ContestClone(ContestMixin, TitleMixin, SingleObjectFormView):
contest.is_visible = False
contest.user_count = 0
contest.key = form.cleaned_data["key"]
contest.is_rated = False
contest.save()
contest.tags.set(tags)
@ -562,12 +657,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
profile = request.profile
if profile.current_contest is not None:
return generic_message(
request,
_("Already in contest"),
_('You are already in a contest: "%s".')
% profile.current_contest.contest.name,
)
profile.remove_contest()
if (
not request.user.is_superuser
@ -646,6 +736,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
profile.save()
contest._updating_stats_only = True
contest.update_user_count()
request.session["contest_mode"] = True
return HttpResponseRedirect(reverse("problem_list"))
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):
continue
problem_idx = codes.index(problem_code)
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)
counter[problem_idx][bin_idx] += count
for i in range(num_problems):
@ -936,7 +1030,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
ContestRankingProfile = namedtuple(
"ContestRankingProfile",
"id user css_class username points cumtime tiebreaker organization participation "
"id user username points cumtime tiebreaker participation "
"participation_rating problem_cells result_cell",
)
@ -956,13 +1050,11 @@ def make_contest_ranking_profile(
user = participation.user
return ContestRankingProfile(
id=user.id,
user=user.user,
css_class=user.css_class,
user=user,
username=user.username,
points=points,
cumtime=cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating
if hasattr(participation, "rating")
else None,
@ -979,35 +1071,51 @@ def make_contest_ranking_profile(
)
def base_contest_ranking_list(contest, problems, queryset, show_final=False):
return [
make_contest_ranking_profile(contest, participation, problems, show_final)
for participation in queryset.select_related("user__user", "rating").defer(
"user__about", "user__organizations__about"
)
def base_contest_ranking_list(
contest, problems, queryset, show_final=False, extra_participation=None
):
participation_fields = [
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)
if not show_final:
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
show_final,
if extra_participation and extra_participation.virtual:
queryset = queryset | contest.users.filter(id=extra_participation.id)
if show_final:
queryset = queryset.order_by(
"is_disqualified", "-score_final", "cumtime_final", "tiebreaker"
)
else:
queryset = queryset.order_by(
"is_disqualified", "-score", "cumtime", "tiebreaker"
)
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score_final, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"),
queryset,
show_final,
)
@ -1017,7 +1125,6 @@ def get_contest_ranking_list(
contest,
participation=None,
ranking_list=contest_ranking_list,
show_current_virtual=False,
ranker=ranker,
show_final=False,
):
@ -1027,20 +1134,16 @@ def get_contest_ranking_list(
.order_by("order")
)
users = ranker(
ranking_list(contest, problems, show_final=show_final),
key=attrgetter("points", "cumtime", "tiebreaker"),
if participation is None:
participation = _get_current_virtual_participation(request, contest)
ranking_list_result = ranking_list(
contest, problems, show_final=show_final, extra_participation=participation
)
if show_current_virtual:
if participation is None and request.user.is_authenticated:
participation = request.profile.current_contest
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,
users = ranker(
ranking_list_result,
key=attrgetter("points", "cumtime", "tiebreaker"),
)
return users, problems
@ -1061,6 +1164,9 @@ def contest_ranking_ajax(request, contest, participation=None):
):
raise Http404()
if participation is None:
participation = _get_current_virtual_participation(request, contest)
queryset = contest.users.filter(virtual__gte=0)
if request.GET.get("friend") == "true" and request.profile:
friends = request.profile.get_friends()
@ -1072,7 +1178,9 @@ def contest_ranking_ajax(request, contest, participation=None):
request,
contest,
participation,
ranking_list=partial(contest_ranking_list, queryset=queryset),
ranking_list=partial(
contest_ranking_list, queryset=queryset, extra_participation=participation
),
show_final=show_final,
)
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):
template_name = "contest/ranking.html"
page_type = None
@ -1182,7 +1303,6 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
return get_contest_ranking_list(
self.request,
self.object,
show_current_virtual=False,
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
ranker=lambda users, key: (
(user.participation.virtual or live_link, user) for user in users
@ -1418,30 +1538,43 @@ def update_contest_mode(request):
ContestsSummaryData = namedtuple(
"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):
paginate_by = 50
template_name = "contest/contests_summary.html"
def get(self, *args, **kwargs):
try:
contests_summary = ContestsSummary.objects.get(key=key)
self.contests_summary = ContestsSummary.objects.get(key=kwargs["key"])
except:
raise Http404()
return super().get(*args, **kwargs)
cache_key = "csv:" + key
context = cache.get(cache_key)
if context:
return render(request, "contest/contests_summary.html", context)
def get_queryset(self):
total_rank = self.contests_summary.results
return total_rank
scores_system = contests_summary.scores
contests = contests_summary.contests.all()
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)
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
user_css_class = {}
for i in range(len(contests)):
contest = contests[i]
users, problems = get_contest_ranking_list(request, contest)
users, problems = get_contest_ranking_list(None, contest)
for rank, user in users:
curr_score = 0
if rank - 1 < len(scores_system):
@ -1452,7 +1585,9 @@ def contests_summary_view(request, key):
sorted_total_points = [
ContestsSummaryData(
user=user,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
points=total_points[user],
point_contests=result_per_contest[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)
total_rank = ranker(sorted_total_points)
context = {
"total_rank": list(total_rank),
"title": _("Contests Summary"),
"contests": contests,
}
cache.set(cache_key, context)
return render(request, "contest/contests_summary.html", context)
return [(rank, item._asdict()) for rank, item in total_rank]
@receiver([post_save, post_delete], sender=ContestsSummary)
def clear_cache(sender, instance, **kwargs):
cache.delete("csv:" + instance.key)
class OfficialContestList(ContestList):
official = True
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 judge.models.course import Course
from django.views.generic import ListView
from django.views.generic import ListView, DetailView, View
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__ = [
"CourseList",
"CourseDetail",
"CourseResource",
"CourseResourceDetail",
"CourseStudentResults",
"CourseEdit",
"CourseResourceDetailEdit",
"CourseResourceEdit",
]
course_directory_file = ""
from judge.models import Course, CourseLesson, Submission, Profile, CourseRole
from judge.models.course import RoleInCourse
from judge.widgets import HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget
from judge.utils.problems import (
user_attempted_ids,
user_completed_ids,
)
class CourseListMixin(object):
def get_queryset(self):
return Course.objects.filter(is_open="true").values()
def max_case_points_per_problem(profile, problems):
# return a dict {problem_id: {case_points, case_total}}
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):
@ -28,12 +72,179 @@ class CourseList(ListView):
def get_context_data(self, **kwargs):
context = super(CourseList, self).get_context_data(**kwargs)
available, enrolling = [], []
for course in Course.objects.filter(is_public=True).filter(is_open=True):
if Course.is_accessible_by(course, self.request.profile):
enrolling.append(course)
else:
available.append(course)
context["available"] = available
context["enrolling"] = enrolling
context["courses"] = Course.get_accessible_courses(self.request.profile)
context["title"] = _("Courses")
context["page_type"] = "list"
return context
class CourseDetailMixin(object):
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

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.utils.translation import ugettext as _
from django.utils.timezone import now
from django.http import Http404
from judge.models import Profile, Notification, NotificationProfile
from judge.models.notification import unseen_notifications_count
from judge.utils.infinite_paginator import InfinitePaginationMixin
__all__ = ["NotificationList"]
class NotificationList(ListView):
class NotificationList(InfinitePaginationMixin, ListView):
model = Notification
context_object_name = "notifications"
template_name = "notification/list.html"
paginate_by = 50
def get_queryset(self):
self.unseen_cnt = unseen_notifications_count(self.request.profile)
self.queryset = Notification.objects.filter(
owner=self.request.profile
).order_by("-id")[:100]
).order_by("-id")
return self.queryset
@ -27,11 +30,13 @@ class NotificationList(ListView):
context = super().get_context_data(**kwargs)
context["unseen_count"] = self.unseen_cnt
context["title"] = _("Notifications (%d unseen)") % context["unseen_count"]
context["has_notifications"] = self.queryset.exists()
context["first_page_href"] = "."
return context
def get(self, 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)
unseen_notifications_count.dirty(self.request.profile)
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.views.problem import ProblemList
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.tasks import rescore_contest
@ -104,15 +104,15 @@ class OrganizationBase(object):
def is_member(self, org=None):
if org is None:
org = self.object
return (
self.request.profile in org if self.request.user.is_authenticated else False
)
if self.request.profile:
return org.is_member(self.request.profile)
return False
def is_admin(self, org=None):
if org is None:
org = self.object
if self.request.profile:
return org.admins.filter(id=self.request.profile.id).exists()
return org.is_admin(self.request.profile)
return False
def can_access(self, org):
@ -131,6 +131,13 @@ class OrganizationMixin(OrganizationBase):
context["can_edit"] = self.can_edit_organization(self.organization)
context["organization"] = self.organization
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:
context.pop("organizations")
return context
@ -215,41 +222,103 @@ class OrganizationHomeView(OrganizationMixin):
organizations=self.organization,
authors=self.request.profile,
).count()
context["top_rated"] = self.organization.members.filter(
is_unlisted=False
).order_by("-rating")[:10]
context["top_scorer"] = self.organization.members.filter(
is_unlisted=False
).order_by("-performance_points")[:10]
context["top_rated"] = (
self.organization.members.filter(is_unlisted=False)
.order_by("-rating")
.only("id", "rating")[: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
class OrganizationList(TitleMixin, ListView, OrganizationBase):
class OrganizationList(
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView, OrganizationBase
):
model = Organization
context_object_name = "organizations"
template_name = "organization/list.html"
title = gettext_lazy("Groups")
paginate_by = 12
all_sorts = frozenset(("name", "member_count"))
default_desc = frozenset(("name", "member_count"))
def get_queryset(self):
return (
def get_default_sort_order(self, request):
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)
.get_queryset()
.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):
context = super(OrganizationList, self).get_context_data(**kwargs)
context["my_organizations"] = []
context["page_type"] = "organizations"
if self.request.profile:
context["my_organizations"] = context["organizations"].filter(
id__in=self.request.profile.organizations.values("id")
)
other_organizations = context["organizations"].exclude(
id__in=context["my_organizations"]
)
context["open_organizations"] = other_organizations.filter(is_open=True)
context["private_organizations"] = other_organizations.filter(is_open=False)
context["first_page_href"] = "."
context["current_tab"] = self.current_tab
context["page_type"] = self.current_tab
context["organization_query"] = self.organization_query
context["selected_order"] = self.request.GET.get("order")
context["all_sort_options"] = [
("name", _("Name (asc.)")),
("-name", _("Name (desc.)")),
("member_count", _("Member count (asc.)")),
("-member_count", _("Member count (desc.)")),
]
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context
@ -274,14 +343,6 @@ class OrganizationHome(OrganizationHomeView, FeedView):
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
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()
visible_contests = (
@ -407,6 +468,7 @@ class OrganizationContests(
def get_queryset(self):
self.org_query = [self.organization_id]
self.hide_organization_contests = False
return super().get_queryset()
def set_editable_contest(self, contest):
@ -417,20 +479,19 @@ class OrganizationContests(
def get_context_data(self, **kwargs):
context = super(OrganizationContests, self).get_context_data(**kwargs)
context["page_type"] = "contests"
context["hide_contest_orgs"] = True
context.pop("organizations")
if self.can_edit_organization(self.organization):
context["create_url"] = reverse(
"organization_contest_add",
args=[self.organization.id, self.organization.slug],
)
for participation in context["active_participations"]:
if self.current_tab == "active":
for participation in context["contests"]:
self.set_editable_contest(participation.contest)
for contest in context["past_contests"]:
self.set_editable_contest(contest)
for contest in context["current_contests"]:
self.set_editable_contest(contest)
for contest in context["future_contests"]:
else:
for contest in context["contests"]:
self.set_editable_contest(contest)
return context
@ -471,6 +532,9 @@ class OrganizationSubmissions(
),
)
def get_title(self):
return _("Submissions in") + f" {self.organization}"
class OrganizationMembershipChange(
LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
@ -516,6 +580,7 @@ class JoinOrganization(OrganizationMembershipChange):
profile.organizations.add(org)
profile.save()
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
Organization.is_member.dirty(org, profile)
class LeaveOrganization(OrganizationMembershipChange):
@ -528,6 +593,7 @@ class LeaveOrganization(OrganizationMembershipChange):
)
profile.organizations.remove(org)
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
Organization.is_member.dirty(org, profile)
class OrganizationRequestForm(Form):
@ -737,7 +803,7 @@ class AddOrganizationMember(
def form_valid(self, form):
new_users = form.cleaned_data["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_user(self.request.user)
return super(AddOrganizationMember, self).form_valid(form)
@ -804,7 +870,7 @@ class EditOrganization(
return form
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form)
@ -836,7 +902,7 @@ class AddOrganization(LoginRequiredMixin, TitleMixin, CreateView):
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
status=400,
)
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
res = super(AddOrganization, self).form_valid(form)
@ -861,7 +927,7 @@ class AddOrganizationContest(
return kwargs
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
@ -954,7 +1020,7 @@ class EditOrganizationContest(
return self.contest
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
res = super(EditOrganizationContest, self).form_valid(form)
@ -974,6 +1040,18 @@ class EditOrganizationContest(
)
):
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
def get_problem_formset(self, post=False):
@ -1015,7 +1093,7 @@ class AddOrganizationBlog(
return _("Add blog for %s") % self.organization.name
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
res = super(AddOrganizationBlog, self).form_valid(form)
self.object.is_organization_private = True
self.object.authors.add(self.request.profile)
@ -1038,6 +1116,11 @@ class AddOrganizationBlog(
)
return res
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
class EditOrganizationBlog(
LoginRequiredMixin,
@ -1061,7 +1144,7 @@ class EditOrganizationBlog(
if self.organization not in self.blog.organizations.all():
raise Exception("This blog does not belong to this organization")
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)
):
raise Exception("Not allowed to edit this blog")
@ -1115,13 +1198,18 @@ class EditOrganizationBlog(
make_notification(posible_users, action, html, self.request.profile)
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
res = super(EditOrganizationBlog, self).form_valid(form)
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
self.create_notification("Edit blog")
return res
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
class PendingBlogs(
LoginRequiredMixin,

View file

@ -8,13 +8,13 @@ from django.http import (
HttpResponseForbidden,
)
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.detail import SingleObjectMixin
from judge.dblock import LockModel
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__ = [
"upvote_page",
@ -24,6 +24,7 @@ __all__ = [
]
@ratelimit(key="user", rate=settings.RL_VOTE)
@login_required
def vote_page(request, delta):
if abs(delta) != 1:
@ -52,8 +53,10 @@ def vote_page(request, delta):
pagevote_id = int(request.POST["id"])
except ValueError:
return HttpResponseBadRequest()
else:
if not PageVote.objects.filter(id=pagevote_id).exists():
try:
pagevote = PageVote.objects.get(id=pagevote_id)
except PageVote.DoesNotExist:
raise Http404()
vote = PageVoteVoter()
@ -61,26 +64,22 @@ def vote_page(request, delta):
vote.voter = request.profile
vote.score = delta
while True:
try:
vote.save()
except IntegrityError:
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
raise Http404()
vote.delete()
PageVote.objects.filter(id=pagevote_id).update(
score=F("score") - vote.score
)
PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
break
_dirty_vote_score(pagevote_id, request.profile)
dirty_pagevote(pagevote, request.profile)
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["pagevote"] = self.object.get_or_create_pagevote()
return context
def _dirty_vote_score(pagevote_id, profile):
pv = PageVote(id=pagevote_id)
pv.vote_score.dirty(pv, profile)

View file

@ -1,10 +1,8 @@
import logging
import os
import shutil
from datetime import timedelta, datetime
from operator import itemgetter
from random import randrange
import random
from copy import deepcopy
from django.core.cache import cache
@ -24,6 +22,7 @@ from django.db.models import (
Q,
When,
IntegerField,
Sum,
)
from django.db.models.functions import Coalesce
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.detail import SingleObjectMixin
from judge.comments import CommentedDetailView
from judge.views.comment import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import (
ContestProblem,
@ -66,6 +65,7 @@ from judge.models import (
Organization,
Profile,
LanguageTemplate,
Contest,
)
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator
@ -77,6 +77,8 @@ from judge.utils.problems import (
user_attempted_ids,
user_completed_ids,
get_related_problems,
get_user_recommended_problems,
RecommendationType,
)
from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter
@ -351,7 +353,7 @@ class ProblemDetail(
else:
context["fileio_input"] = 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(
self.profile, self.object
)
@ -399,16 +401,13 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
if trans is None
else trans.description,
"url": request.build_absolute_uri(),
"math_engine": maker.math_engine,
}
)
.replace('"//', '"https://')
.replace("'//", "'https://")
)
maker.title = problem_name
assets = ["style.css", "pygment-github.css"]
if maker.math_engine == "jax":
assets.append("mathjax3_config.js")
assets = ["style.css"]
for file in assets:
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make()
@ -590,7 +589,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
i
for i in query
if i in self.profile.organizations.values_list("id", flat=True)
]
][:3]
def get_normal_queryset(self):
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]
if 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(
Q(organizations__in=self.org_query)
| Q(contests__contest__organizations__in=self.org_query)
Q(organizations__in=self.org_query) | Q(id__in=contest_problems)
)
if 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)
if self.point_end is not None:
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()
def get_queryset(self):
@ -664,12 +678,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
if self.request.profile:
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["categories"] = ProblemGroup.objects.all()
if self.show_types:
@ -677,7 +685,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
context["problem_types"] = ProblemType.objects.all()
context["has_fts"] = settings.ENABLE_FTS
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["completed_problem_ids"] = self.get_completed_problems()
context["attempted_problems"] = self.get_attempted_problems()
@ -829,29 +837,39 @@ class ProblemFeed(ProblemList, FeedView):
model = Problem
context_object_name = "problems"
template_name = "problem/feed.html"
feed_content_template_name = "problem/feed/problems.html"
feed_content_template_name = "problem/feed/items.html"
paginate_by = 4
title = _("Problem feed")
feed_type = None
# arr = [[], [], ..]
def merge_recommendation(self, arr):
seed = datetime.now().strftime("%d%m%Y")
merged_array = []
for a in arr:
merged_array += a
random.Random(seed).shuffle(merged_array)
def get_recommended_problem_ids(self, queryset):
user_id = self.request.profile.id
problem_ids = queryset.values_list("id", flat=True)
rec_types = [
RecommendationType.CF_DOT,
RecommendationType.CF_COSINE,
RecommendationType.CF_TIME_DOT,
RecommendationType.CF_TIME_COSINE,
RecommendationType.HOT_PROBLEM,
]
limits = [100, 100, 100, 100, 20]
shuffle = True
res = []
used_pid = set()
allow_debug_type = (
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:
if type(obj) == tuple:
obj = obj[1]
if obj not in used_pid:
res.append(obj)
used_pid.add(obj)
return res
return get_user_recommended_problems(
user_id, problem_ids, rec_types, limits, shuffle
)
def get_queryset(self):
if self.feed_type == "volunteer":
@ -885,40 +903,8 @@ class ProblemFeed(ProblemList, FeedView):
if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
cf_model = CollabFilter("collab_filter")
cf_time_model = CollabFilter("collab_filter_time")
q = self.get_recommended_problem_ids(queryset)
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 = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
@ -974,6 +960,12 @@ class LanguageTemplateAjax(View):
class RandomProblem(ProblemList):
def get(self, request, *args, **kwargs):
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:
raise Http404()
@ -994,6 +986,15 @@ class RandomProblem(ProblemList):
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
def problem_submit(request, problem, submission=None):
if (
@ -1042,7 +1043,7 @@ def problem_submit(request, problem, submission=None):
>= settings.DMOJ_SUBMISSION_LIMIT
):
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(
id=form.cleaned_data["language"].id
@ -1063,7 +1064,22 @@ def problem_submit(request, problem, submission=None):
with transaction.atomic():
if profile.current_contest is not None:
contest = profile.current_contest.contest
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:
contest_problem = problem.contests.get(contest_id=contest_id)
except ContestProblem.DoesNotExist:
@ -1143,11 +1159,11 @@ def problem_submit(request, problem, submission=None):
default_lang = request.profile.language
submission_limit = submissions_left = None
next_valid_submit_time = None
if profile.current_contest is not None:
try:
submission_limit = problem.contests.get(
contest = profile.current_contest.contest
).max_submissions
try:
submission_limit = problem.contests.get(contest=contest).max_submissions
except ContestProblem.DoesNotExist:
pass
else:
@ -1155,6 +1171,12 @@ def problem_submit(request, problem, submission=None):
submissions_left = submission_limit - get_contest_submission_count(
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(
request,
"problem/submit.html",
@ -1184,6 +1206,7 @@ def problem_submit(request, problem, submission=None):
"output_only": problem.data_files.output_only
if hasattr(problem, "data_files")
else False,
"next_valid_submit_time": next_valid_submit_time,
},
)
@ -1208,7 +1231,7 @@ class ProblemClone(
problem.ac_rate = 0
problem.user_count = 0
problem.code = form.cleaned_data["code"]
problem.save()
problem.save(should_move_data=False)
problem.authors.add(self.request.profile)
problem.allowed_languages.set(languages)
problem.language_limits.set(language_limits)

View file

@ -89,7 +89,7 @@ class ProblemDataForm(ModelForm):
"checker",
"checker_args",
"custom_checker",
"custom_validator",
"custom_checker_cpp",
"interactive_judge",
"fileio_input",
"fileio_output",
@ -344,7 +344,7 @@ def problem_init_view(request, problem):
"problem/yaml.html",
{
"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,
"content_title": mark_safe(
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["in_contest"] = False
context["current_contest"] = None
if (
self.request.in_contest_mode
and self.object in self.request.participation.contest.problems.all()
):
context["in_contest"] = True
context["current_contest"] = self.request.participation.contest
return context
@ -106,20 +106,12 @@ class BaseActionSubmissionsView(
try:
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:
return HttpResponseBadRequest()
contest = None
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
)
return self.generate_response(id_range, languages, results, contests)
def generate_response(self, id_range, languages, results, contest):
raise NotImplementedError()

View file

@ -1,6 +1,6 @@
from django.views.generic import TemplateView
from django.utils.translation import gettext as _
from django.http import HttpResponseForbidden
from django.http import HttpResponseForbidden, JsonResponse
from judge.models import Contest
from django.utils.safestring import mark_safe
@ -21,7 +21,7 @@ class Resolver(TemplateView):
hidden_subtasks = self.contest.format.get_hidden_subtasks()
num_problems = len(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)}
users = {}
@ -126,10 +126,8 @@ class Resolver(TemplateView):
for i in hidden_subtasks:
order = id_to_order[i]
if hidden_subtasks[i]:
sub_frozen[order - 1] = min(hidden_subtasks[i])
else:
sub_frozen[order - 1] = problem_sub[order - 1] + 1
sub_frozen[order - 1] = list(hidden_subtasks[i])
return {
"problem_sub": problem_sub,
"sub_frozen": sub_frozen,
@ -143,8 +141,15 @@ class Resolver(TemplateView):
return context
def get(self, request, *args, **kwargs):
if request.user.is_superuser:
self.contest = Contest.objects.get(key=kwargs.get("contest"))
if self.contest.format.has_hidden_subtasks:
return super(Resolver, self).get(request, *args, **kwargs)
if not request.user.is_superuser:
return HttpResponseForbidden()
self.contest = Contest.objects.get(key=kwargs.get("contest"))
if not self.contest.format.has_hidden_subtasks:
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):
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):
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)
)
class CommentSelect2View(Select2View):
def get_queryset(self):
return Comment.objects.filter(page__icontains=self.term)
if self.problem_id:
q = q.filter(problems=self.problem_id)
return q
class UserSearchSelect2View(BaseListView):
@ -193,3 +195,17 @@ class ChatUserSearchSelect2View(UserSearchSelect2View):
),
"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.highlight_code import highlight_code
from judge.models import Contest, ContestParticipation
from judge.models import Language
from judge.models import Problem
from judge.models import ProblemTestCase
from judge.models import ProblemTranslation
from judge.models import Profile
from judge.models import Submission
from judge.models import (
Contest,
ContestParticipation,
Language,
Problem,
ProblemTestCase,
ProblemTranslation,
Profile,
Submission,
)
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.raw_sql import join_sql_subquery, use_straight_join
from judge.utils.views import DiggPaginatorMixin
from judge.utils.infinite_paginator import InfinitePaginationMixin
from judge.utils.views import TitleMixin
from judge.utils.timedelta import nice_repr
from judge.views.contests import ContestMixin
from judge.caching import cache_wrapper
def submission_related(queryset):
return queryset.select_related("user__user", "problem", "language").only(
return queryset.select_related("user", "problem", "language").only(
"id",
"user__user__username",
"user__display_rank",
"user__rating",
"user__id",
"problem__name",
"problem__code",
"problem__is_public",
@ -70,7 +72,8 @@ def submission_related(queryset):
"case_points",
"case_total",
"current_testcase",
"contest_object",
"contest_object__key",
"contest_object__name",
)
@ -81,6 +84,10 @@ class SubmissionMixin(object):
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):
submission = super(SubmissionDetailBase, self).get_object(queryset)
if submission.is_accessible_by(self.request.profile):
@ -92,7 +99,7 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta
submission = self.object
return _("Submission of %(problem)s by %(user)s") % {
"problem": submission.problem.translated_name(self.request.LANGUAGE_CODE),
"user": submission.user.user.username,
"user": submission.user.username,
}
def get_content_title(self):
@ -107,29 +114,13 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta
),
"user": format_html(
'<a href="{0}">{1}</a>',
reverse("user_page", args=[submission.user.user.username]),
submission.user.user.username,
reverse("user_page", args=[submission.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):
contest = submission.contest_object
if contest and contest.is_editable_by(request.user):
@ -205,15 +196,28 @@ def get_cases_data(submission):
class SubmissionStatus(SubmissionDetailBase):
template_name = "submission/status.html"
def access_testcases_in_contest(self):
contest = self.object.contest_or_none
if contest is None:
return False
if contest.problem.problem.is_editable_by(self.request.user):
def can_see_testcases(self):
contest_submission = self.object.contest_or_none
if contest_submission is None:
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
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 False
@ -228,19 +232,14 @@ class SubmissionStatus(SubmissionDetailBase):
)
context["time_limit"] = submission.problem.time_limit
context["can_see_testcases"] = False
context["raw_source"] = submission.source.source.rstrip("\n")
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
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:
if self.can_see_testcases():
context["cases_data"] = get_cases_data(submission)
context["can_see_testcases"] = True
try:
@ -266,7 +265,7 @@ class SubmissionTestCaseQuery(SubmissionStatus):
return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs)
class SubmissionSourceRaw(SubmissionSource):
class SubmissionSourceRaw(SubmissionDetailBase):
def get(self, request, *args, **kwargs):
submission = self.get_object()
return HttpResponse(submission.source.source, content_type="text/plain")
@ -311,6 +310,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
def access_check(self, request):
pass
def hide_contest_in_row(self):
return self.request.in_contest_mode
@cached_property
def in_contest(self):
return (
@ -379,17 +381,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
)
if self.selected_languages:
# Note (DMOJ): MariaDB can't optimize this subquery for some insane, unknown reason,
# 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)
)
)
queryset = queryset.filter(language__in=self.selected_languages)
if self.selected_statuses:
submission_results = [i for i, _ in Submission.RESULT]
if self.selected_statuses[0] in submission_results:
@ -460,7 +452,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
context["show_problem"] = self.show_problem
context["profile"] = self.request.profile
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["selected_statuses"] = self.selected_statuses
@ -480,11 +472,16 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
context["friend_submissions_link"] = self.get_friend_submissions_page()
context["all_submissions_link"] = self.get_all_submissions_page()
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()
if context["in_hidden_subtasks_contest"]:
for submission in context["submissions"]:
self.modify_attrs(submission)
context[
"is_in_editable_contest"
] = self.in_contest and self.contest.is_editable_by(self.request.user)
return context
def get(self, request, *args, **kwargs):
@ -494,6 +491,19 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
self.selected_languages = request.GET.getlist("language")
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):
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)
)
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):
raise Http404()
@ -748,6 +763,7 @@ def single_submission(request, submission_id, show_problem=True):
"problem_name": show_problem
and submission.problem.translated_name(request.LANGUAGE_CODE),
"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:
return super(AllSubmissions, self)._get_result_data()
key = "global_submission_result_data"
if self.selected_statuses:
key += ":" + ",".join(self.selected_statuses)
if self.selected_languages:
key += ":" + ",".join(self.selected_languages)
result = cache.get(key)
if result:
return result
queryset = Submission.objects
if self.selected_languages:
queryset = queryset.filter(
language__in=Language.objects.filter(key__in=self.selected_languages)
return _get_global_submission_result_data(
self.selected_statuses, 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):
@ -842,6 +839,38 @@ class ForceContestMixin(object):
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):
check_contest_in_access_check = True
@ -1027,3 +1056,19 @@ class SubmissionSourceFileView(View):
response["Content-Type"] = "application/octet-stream"
response["Content-Disposition"] = "attachment; filename=%s" % (filename,)
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