Merge branch 'LQDJudge:master' into master
This commit is contained in:
commit
ff9b86ea13
2370 changed files with 30872 additions and 13914 deletions
2
502.html
2
502.html
|
@ -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>
|
||||
|
|
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
|||
|
||||
class ChatBoxConfig(AppConfig):
|
||||
name = "chat_box"
|
||||
|
||||
def ready(self):
|
||||
from . import models
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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())
|
||||
|
|
110
dmoj/urls.py
110
dmoj/urls.py
|
@ -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"):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
52
judge/admin/course.py
Normal 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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",)
|
||||
|
|
|
@ -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
48
judge/authentication.py
Normal 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
|
|
@ -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))
|
||||
|
|
|
@ -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
2
judge/bridge/utils.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
class VanishedSubmission(Exception):
|
||||
pass
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
22
judge/custom_translations.py
Normal file
22
judge/custom_translations.py
Normal 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 didn’t match."),
|
||||
_("Your password can’t be entirely numeric."),
|
||||
# Navbar
|
||||
_("Bug Report"),
|
||||
_("Courses"),
|
||||
]
|
120
judge/feed.py
120
judge/feed.py
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -22,6 +22,7 @@ from . import (
|
|||
social,
|
||||
spaceless,
|
||||
timedelta,
|
||||
comment,
|
||||
)
|
||||
from . import registry
|
||||
|
||||
|
|
12
judge/jinja2/comment.py
Normal file
12
judge/jinja2/comment.py
Normal 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)
|
|
@ -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}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
149
judge/markdown.py
Normal 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
|
2
judge/markdown_extensions/__init__.py
Normal file
2
judge/markdown_extensions/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .youtube import YouTubeExtension
|
||||
from .emoticon import EmoticonExtension
|
112
judge/markdown_extensions/emoticon.py
Normal file
112
judge/markdown_extensions/emoticon.py
Normal 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)
|
36
judge/markdown_extensions/youtube.py
Normal file
36
judge/markdown_extensions/youtube.py
Normal 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)
|
50
judge/migrations/0174_contest_summary_result.py
Normal file
50
judge/migrations/0174_contest_summary_result.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
20
judge/migrations/0175_add_profile_index.py
Normal file
20
judge/migrations/0175_add_profile_index.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
30
judge/migrations/0176_comment_revision_count.py
Normal file
30
judge/migrations/0176_comment_revision_count.py
Normal 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
|
||||
# ),
|
||||
]
|
35
judge/migrations/0177_test_formatter.py
Normal file
35
judge/migrations/0177_test_formatter.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
17
judge/migrations/0178_remove_user_script.py
Normal file
17
judge/migrations/0178_remove_user_script.py
Normal 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",
|
||||
),
|
||||
]
|
19
judge/migrations/0179_submission_result_lang_index.py
Normal file
19
judge/migrations/0179_submission_result_lang_index.py
Normal 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"
|
||||
),
|
||||
),
|
||||
]
|
78
judge/migrations/0180_course.py
Normal file
78
judge/migrations/0180_course.py
Normal 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"),
|
||||
),
|
||||
]
|
50
judge/migrations/0181_remove_math_engine.py
Normal file
50
judge/migrations/0181_remove_math_engine.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
75
judge/migrations/0182_rename_customcpp.py
Normal file
75
judge/migrations/0182_rename_customcpp.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
45
judge/migrations/0183_rename_custom_checker_cpp.py
Normal file
45
judge/migrations/0183_rename_custom_checker_cpp.py
Normal 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",
|
||||
),
|
||||
]
|
28
judge/migrations/0184_contest_rate_limit.py
Normal file
28
judge/migrations/0184_contest_rate_limit.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
18
judge/migrations/0185_rename_org_profile_colum.py
Normal file
18
judge/migrations/0185_rename_org_profile_colum.py
Normal 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",
|
||||
),
|
||||
]
|
43
judge/migrations/0186_change_about_fields_max_len.py
Normal file
43
judge/migrations/0186_change_about_fields_max_len.py
Normal 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"
|
||||
),
|
||||
),
|
||||
]
|
69
judge/migrations/0187_profile_info.py
Normal file
69
judge/migrations/0187_profile_info.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
110
judge/migrations/0188_official_contest.py
Normal file
110
judge/migrations/0188_official_contest.py
Normal 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",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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]
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]),
|
||||
]
|
||||
|
||||
|
||||
|
|
26
judge/models/test_formatter.py
Normal file
26
judge/models/test_formatter.py
Normal 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,
|
||||
)
|
|
@ -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",
|
||||
|
|
|
@ -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,)))
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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?")
|
||||
|
|
|
@ -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
67
judge/utils/users.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
43
judge/views/custom_file_upload.py
Normal file
43
judge/views/custom_file_upload.py
Normal 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")},
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
contest = profile.current_contest.contest
|
||||
try:
|
||||
submission_limit = problem.contests.get(
|
||||
contest=profile.current_contest.contest
|
||||
).max_submissions
|
||||
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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
207
judge/views/test_formatter/test_formatter.py
Normal file
207
judge/views/test_formatter/test_formatter.py
Normal 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
|
116
judge/views/test_formatter/tf_logic.py
Normal file
116
judge/views/test_formatter/tf_logic.py
Normal 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
|
268
judge/views/test_formatter/tf_pattern.py
Normal file
268
judge/views/test_formatter/tf_pattern.py
Normal 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))
|
15
judge/views/test_formatter/tf_utils.py
Normal file
15
judge/views/test_formatter/tf_utils.py
Normal 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
Loading…
Reference in a new issue