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>
|
<br>
|
||||||
<div class="popup">
|
<div class="popup">
|
||||||
<div>
|
<div>
|
||||||
<img class="logo" src="logo.png" alt="LQDOJ">
|
<img class="logo" src="logo.svg" alt="LQDOJ">
|
||||||
</div>
|
</div>
|
||||||
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
|
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class ChatBoxConfig(AppConfig):
|
class ChatBoxConfig(AppConfig):
|
||||||
name = "chat_box"
|
name = "chat_box"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import models
|
||||||
|
|
|
@ -25,26 +25,18 @@ class Room(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = "chat_box"
|
app_label = "chat_box"
|
||||||
|
|
||||||
@cache_wrapper(prefix="Rinfo")
|
|
||||||
def _info(self):
|
|
||||||
last_msg = self.message_set.first()
|
|
||||||
return {
|
|
||||||
"user_ids": [self.user_one.id, self.user_two.id],
|
|
||||||
"last_message": last_msg.body if last_msg else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _cached_info(self):
|
def _cached_info(self):
|
||||||
return self._info()
|
return get_room_info(self.id)
|
||||||
|
|
||||||
def contain(self, profile):
|
def contain(self, profile):
|
||||||
return profile.id in self._cached_info["user_ids"]
|
return profile.id in [self.user_one_id, self.user_two_id]
|
||||||
|
|
||||||
def other_user(self, profile):
|
def other_user(self, profile):
|
||||||
return self.user_one if profile == self.user_two else self.user_two
|
return self.user_one if profile == self.user_two else self.user_two
|
||||||
|
|
||||||
def other_user_id(self, profile):
|
def other_user_id(self, profile):
|
||||||
user_ids = self._cached_info["user_ids"]
|
user_ids = [self.user_one_id, self.user_two_id]
|
||||||
return sum(user_ids) - profile.id
|
return sum(user_ids) - profile.id
|
||||||
|
|
||||||
def users(self):
|
def users(self):
|
||||||
|
@ -53,6 +45,10 @@ class Room(models.Model):
|
||||||
def last_message_body(self):
|
def last_message_body(self):
|
||||||
return self._cached_info["last_message"]
|
return self._cached_info["last_message"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def prefetch_room_cache(self, room_ids):
|
||||||
|
get_room_info.prefetch_multi([(i,) for i in room_ids])
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
||||||
|
@ -66,7 +62,6 @@ class Message(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
new_message = self.id
|
|
||||||
self.body = self.body.strip()
|
self.body = self.body.strip()
|
||||||
super(Message, self).save(*args, **kwargs)
|
super(Message, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -148,3 +143,11 @@ class Ignore(models.Model):
|
||||||
self.remove_ignore(current_user, friend)
|
self.remove_ignore(current_user, friend)
|
||||||
else:
|
else:
|
||||||
self.add_ignore(current_user, friend)
|
self.add_ignore(current_user, friend)
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Rinfo")
|
||||||
|
def get_room_info(room_id):
|
||||||
|
last_msg = Message.objects.filter(room_id=room_id).first()
|
||||||
|
return {
|
||||||
|
"last_message": last_msg.body if last_msg else None,
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ from judge import event_poster as event
|
||||||
from judge.jinja2.gravatar import gravatar
|
from judge.jinja2.gravatar import gravatar
|
||||||
from judge.models import Friend
|
from judge.models import Friend
|
||||||
|
|
||||||
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
|
from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info
|
||||||
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
|
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
|
||||||
|
|
||||||
|
|
||||||
|
@ -174,19 +174,46 @@ def mute_message(request):
|
||||||
return JsonResponse(ret)
|
return JsonResponse(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def check_valid_message(request, room):
|
||||||
|
if not room and len(request.POST["body"]) > 200:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not can_access_room(request, room) or request.profile.mute:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_msg = Message.objects.filter(room=room).first()
|
||||||
|
if (
|
||||||
|
last_msg
|
||||||
|
and last_msg.author == request.profile
|
||||||
|
and last_msg.body == request.POST["body"].strip()
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not room:
|
||||||
|
four_last_msg = Message.objects.filter(room=room).order_by("-id")[:4]
|
||||||
|
if len(four_last_msg) >= 4:
|
||||||
|
same_author = all(msg.author == request.profile for msg in four_last_msg)
|
||||||
|
time_diff = timezone.now() - four_last_msg[3].time
|
||||||
|
if same_author and time_diff.total_seconds() < 300:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def post_message(request):
|
def post_message(request):
|
||||||
ret = {"msg": "posted"}
|
ret = {"msg": "posted"}
|
||||||
|
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
if len(request.POST["body"]) > 5000:
|
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
room = None
|
room = None
|
||||||
if request.POST["room"]:
|
if request.POST["room"]:
|
||||||
room = Room.objects.get(id=request.POST["room"])
|
room = Room.objects.get(id=request.POST["room"])
|
||||||
|
|
||||||
if not can_access_room(request, room) or request.profile.mute:
|
if not check_valid_message(request, room):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
|
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
|
||||||
|
@ -204,7 +231,7 @@ def post_message(request):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
Room._info.dirty(room)
|
get_room_info.dirty(room.id)
|
||||||
room.last_msg_time = new_message.time
|
room.last_msg_time = new_message.time
|
||||||
room.save()
|
room.save()
|
||||||
|
|
||||||
|
@ -229,9 +256,7 @@ def post_message(request):
|
||||||
|
|
||||||
|
|
||||||
def can_access_room(request, room):
|
def can_access_room(request, room):
|
||||||
return (
|
return not room or room.contain(request.profile)
|
||||||
not room or room.user_one == request.profile or room.user_two == request.profile
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -247,7 +272,7 @@ def chat_message_ajax(request):
|
||||||
try:
|
try:
|
||||||
message = Message.objects.filter(hidden=False).get(id=message_id)
|
message = Message.objects.filter(hidden=False).get(id=message_id)
|
||||||
room = message.room
|
room = message.room
|
||||||
if room and not room.contain(request.profile):
|
if not can_access_room(request, room):
|
||||||
return HttpResponse("Unauthorized", status=401)
|
return HttpResponse("Unauthorized", status=401)
|
||||||
except Message.DoesNotExist:
|
except Message.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
@ -278,7 +303,7 @@ def update_last_seen(request, **kwargs):
|
||||||
except Room.DoesNotExist:
|
except Room.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
if room and not room.contain(profile):
|
if not can_access_room(request, room):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
||||||
|
@ -338,6 +363,8 @@ def user_online_status_ajax(request):
|
||||||
def get_online_status(profile, other_profile_ids, rooms=None):
|
def get_online_status(profile, other_profile_ids, rooms=None):
|
||||||
if not other_profile_ids:
|
if not other_profile_ids:
|
||||||
return None
|
return None
|
||||||
|
Profile.prefetch_profile_cache(other_profile_ids)
|
||||||
|
|
||||||
joined_ids = ",".join([str(id) for id in other_profile_ids])
|
joined_ids = ",".join([str(id) for id in other_profile_ids])
|
||||||
other_profiles = Profile.objects.raw(
|
other_profiles = Profile.objects.raw(
|
||||||
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
|
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
|
||||||
|
@ -404,6 +431,7 @@ def get_status_context(profile, include_ignored=False):
|
||||||
|
|
||||||
recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
|
recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
|
||||||
recent_rooms = [int(i["id"]) for i in recent_profile]
|
recent_rooms = [int(i["id"]) for i in recent_profile]
|
||||||
|
Room.prefetch_room_cache(recent_rooms)
|
||||||
|
|
||||||
admin_list = (
|
admin_list = (
|
||||||
queryset.filter(display_rank="admin")
|
queryset.filter(display_rank="admin")
|
||||||
|
@ -473,9 +501,16 @@ def get_or_create_room(request):
|
||||||
user_room.last_seen = timezone.now()
|
user_room.last_seen = timezone.now()
|
||||||
user_room.save()
|
user_room.save()
|
||||||
|
|
||||||
|
room_url = reverse("chat", kwargs={"room_id": room.id})
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return JsonResponse({"room": room.id, "other_user_id": other_user.id})
|
return JsonResponse(
|
||||||
return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id}))
|
{
|
||||||
|
"room": room.id,
|
||||||
|
"other_user_id": other_user.id,
|
||||||
|
"url": room_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(room_url)
|
||||||
|
|
||||||
|
|
||||||
def get_unread_count(rooms, user):
|
def get_unread_count(rooms, user):
|
||||||
|
|
|
@ -34,6 +34,7 @@ SITE_ID = 1
|
||||||
SITE_NAME = "LQDOJ"
|
SITE_NAME = "LQDOJ"
|
||||||
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
|
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
|
||||||
SITE_ADMIN_EMAIL = False
|
SITE_ADMIN_EMAIL = False
|
||||||
|
SITE_DOMAIN = "lqdoj.edu.vn"
|
||||||
|
|
||||||
DMOJ_REQUIRE_STAFF_2FA = True
|
DMOJ_REQUIRE_STAFF_2FA = True
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||||
"ERR": "#ffa71c",
|
"ERR": "#ffa71c",
|
||||||
}
|
}
|
||||||
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
||||||
|
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
|
||||||
|
|
||||||
MARKDOWN_STYLES = {}
|
MARKDOWN_STYLES = {}
|
||||||
MARKDOWN_DEFAULT_STYLE = {}
|
MARKDOWN_DEFAULT_STYLE = {}
|
||||||
|
@ -130,13 +132,10 @@ USE_SELENIUM = False
|
||||||
SELENIUM_CUSTOM_CHROME_PATH = None
|
SELENIUM_CUSTOM_CHROME_PATH = None
|
||||||
SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
|
SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
|
||||||
|
|
||||||
PYGMENT_THEME = "pygment-github.css"
|
|
||||||
INLINE_JQUERY = True
|
INLINE_JQUERY = True
|
||||||
INLINE_FONTAWESOME = True
|
INLINE_FONTAWESOME = True
|
||||||
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
|
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
|
||||||
FONTAWESOME_CSS = (
|
FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
|
||||||
)
|
|
||||||
DMOJ_CANONICAL = ""
|
DMOJ_CANONICAL = ""
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
@ -170,7 +169,7 @@ else:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "judge.Submission",
|
"model": "judge.Submission",
|
||||||
"icon": "fa-check-square-o",
|
"icon": "fa-check-square",
|
||||||
"children": [
|
"children": [
|
||||||
"judge.Language",
|
"judge.Language",
|
||||||
"judge.Judge",
|
"judge.Judge",
|
||||||
|
@ -278,8 +277,11 @@ LANGUAGE_COOKIE_AGE = 8640000
|
||||||
|
|
||||||
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
||||||
|
|
||||||
IMPERSONATE_REQUIRE_SUPERUSER = True
|
IMPERSONATE = {
|
||||||
IMPERSONATE_DISABLE_LOGGING = True
|
"REQUIRE_SUPERUSER": True,
|
||||||
|
"DISABLE_LOGGING": True,
|
||||||
|
"ADMIN_DELETE_PERMISSION": True,
|
||||||
|
}
|
||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
|
@ -323,7 +325,6 @@ TEMPLATES = [
|
||||||
"judge.template_context.site",
|
"judge.template_context.site",
|
||||||
"judge.template_context.site_name",
|
"judge.template_context.site_name",
|
||||||
"judge.template_context.misc_config",
|
"judge.template_context.misc_config",
|
||||||
"judge.template_context.math_setting",
|
|
||||||
"social_django.context_processors.backends",
|
"social_django.context_processors.backends",
|
||||||
"social_django.context_processors.login_redirect",
|
"social_django.context_processors.login_redirect",
|
||||||
],
|
],
|
||||||
|
@ -431,7 +432,7 @@ AUTHENTICATION_BACKENDS = (
|
||||||
"social_core.backends.google.GoogleOAuth2",
|
"social_core.backends.google.GoogleOAuth2",
|
||||||
"social_core.backends.facebook.FacebookOAuth2",
|
"social_core.backends.facebook.FacebookOAuth2",
|
||||||
"judge.social_auth.GitHubSecureEmailOAuth2",
|
"judge.social_auth.GitHubSecureEmailOAuth2",
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
"judge.authentication.CustomModelBackend",
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCIAL_AUTH_PIPELINE = (
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
|
@ -488,6 +489,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
# Chunk upload
|
# Chunk upload
|
||||||
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
|
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
|
||||||
|
|
||||||
|
# Rate limit
|
||||||
|
RL_VOTE = "200/h"
|
||||||
|
RL_COMMENT = "30/h"
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
|
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
|
||||||
exec(f.read(), globals())
|
exec(f.read(), globals())
|
||||||
|
|
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 django.conf.urls.static import static as url_static
|
||||||
|
|
||||||
|
|
||||||
from judge.feed import (
|
|
||||||
AtomBlogFeed,
|
|
||||||
AtomCommentFeed,
|
|
||||||
AtomProblemFeed,
|
|
||||||
BlogFeed,
|
|
||||||
CommentFeed,
|
|
||||||
ProblemFeed,
|
|
||||||
)
|
|
||||||
from judge.forms import CustomAuthenticationForm
|
from judge.forms import CustomAuthenticationForm
|
||||||
from judge.sitemap import (
|
from judge.sitemap import (
|
||||||
BlogPostSitemap,
|
BlogPostSitemap,
|
||||||
|
@ -46,6 +38,7 @@ from judge.views import (
|
||||||
license,
|
license,
|
||||||
mailgun,
|
mailgun,
|
||||||
markdown_editor,
|
markdown_editor,
|
||||||
|
test_formatter,
|
||||||
notification,
|
notification,
|
||||||
organization,
|
organization,
|
||||||
preview,
|
preview,
|
||||||
|
@ -68,7 +61,12 @@ from judge.views import (
|
||||||
resolver,
|
resolver,
|
||||||
course,
|
course,
|
||||||
email,
|
email,
|
||||||
|
custom_file_upload,
|
||||||
)
|
)
|
||||||
|
from judge import authentication
|
||||||
|
|
||||||
|
from judge.views.test_formatter import test_formatter
|
||||||
|
|
||||||
from judge.views.problem_data import (
|
from judge.views.problem_data import (
|
||||||
ProblemDataView,
|
ProblemDataView,
|
||||||
ProblemSubmissionDiff,
|
ProblemSubmissionDiff,
|
||||||
|
@ -80,7 +78,6 @@ from judge.views.register import ActivationView, RegistrationView
|
||||||
from judge.views.select2 import (
|
from judge.views.select2 import (
|
||||||
AssigneeSelect2View,
|
AssigneeSelect2View,
|
||||||
ChatUserSearchSelect2View,
|
ChatUserSearchSelect2View,
|
||||||
CommentSelect2View,
|
|
||||||
ContestSelect2View,
|
ContestSelect2View,
|
||||||
ContestUserSearchSelect2View,
|
ContestUserSearchSelect2View,
|
||||||
OrganizationSelect2View,
|
OrganizationSelect2View,
|
||||||
|
@ -88,6 +85,7 @@ from judge.views.select2 import (
|
||||||
TicketUserSelect2View,
|
TicketUserSelect2View,
|
||||||
UserSearchSelect2View,
|
UserSearchSelect2View,
|
||||||
UserSelect2View,
|
UserSelect2View,
|
||||||
|
ProblemAuthorSearchSelect2View,
|
||||||
)
|
)
|
||||||
|
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
@ -144,9 +142,7 @@ register_patterns = [
|
||||||
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
|
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
|
||||||
url(
|
url(
|
||||||
r"^password/change/$",
|
r"^password/change/$",
|
||||||
auth_views.PasswordChangeView.as_view(
|
authentication.CustomPasswordChangeView.as_view(),
|
||||||
template_name="registration/password_change_form.html",
|
|
||||||
),
|
|
||||||
name="password_change",
|
name="password_change",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
|
@ -403,7 +399,28 @@ urlpatterns = [
|
||||||
name="submission_status",
|
name="submission_status",
|
||||||
),
|
),
|
||||||
url(r"^/abort$", submission.abort_submission, name="submission_abort"),
|
url(r"^/abort$", submission.abort_submission, name="submission_abort"),
|
||||||
url(r"^/html$", submission.single_submission),
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^test_formatter/",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
url(
|
||||||
|
r"^$",
|
||||||
|
login_required(test_formatter.TestFormatter.as_view()),
|
||||||
|
name="test_formatter",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^edit_page$",
|
||||||
|
login_required(test_formatter.EditTestFormatter.as_view()),
|
||||||
|
name="test_formatter_edit",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^download_page$",
|
||||||
|
login_required(test_formatter.DownloadTestFormatter.as_view()),
|
||||||
|
name="test_formatter_download",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -471,6 +488,7 @@ urlpatterns = [
|
||||||
reverse("all_user_submissions", args=[user])
|
reverse("all_user_submissions", args=[user])
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
|
||||||
url(
|
url(
|
||||||
r"^/$",
|
r"^/$",
|
||||||
lambda _, user: HttpResponsePermanentRedirect(
|
lambda _, user: HttpResponsePermanentRedirect(
|
||||||
|
@ -519,11 +537,37 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
||||||
url(
|
url(
|
||||||
r"^contests/summary/(?P<key>\w+)$",
|
r"^contests/summary/(?P<key>\w+)/",
|
||||||
contests.contests_summary_view,
|
paged_list_view(contests.ContestsSummaryView, "contests_summary"),
|
||||||
name="contests_summary",
|
),
|
||||||
|
url(
|
||||||
|
r"^contests/official",
|
||||||
|
paged_list_view(contests.OfficialContestList, "official_contest_list"),
|
||||||
|
),
|
||||||
|
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
|
||||||
|
url(
|
||||||
|
r"^course/(?P<slug>[\w-]*)",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
url(r"^$", course.CourseDetail.as_view(), name="course_detail"),
|
||||||
|
url(
|
||||||
|
r"^/lesson/(?P<id>\d+)$",
|
||||||
|
course.CourseLessonDetail.as_view(),
|
||||||
|
name="course_lesson_detail",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/edit_lessons$",
|
||||||
|
course.EditCourseLessonsView.as_view(),
|
||||||
|
name="edit_course_lessons",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/grades$",
|
||||||
|
course.CourseStudentResults.as_view(),
|
||||||
|
name="course_grades",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
),
|
),
|
||||||
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
|
||||||
url(
|
url(
|
||||||
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
||||||
contests.ContestCalendar.as_view(),
|
contests.ContestCalendar.as_view(),
|
||||||
|
@ -587,6 +631,13 @@ urlpatterns = [
|
||||||
"contest_user_submissions_ajax",
|
"contest_user_submissions_ajax",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^/submissions",
|
||||||
|
paged_list_view(
|
||||||
|
submission.ContestSubmissions,
|
||||||
|
"contest_submissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^/participations$",
|
r"^/participations$",
|
||||||
contests.ContestParticipationList.as_view(),
|
contests.ContestParticipationList.as_view(),
|
||||||
|
@ -852,6 +903,11 @@ urlpatterns = [
|
||||||
AssigneeSelect2View.as_view(),
|
AssigneeSelect2View.as_view(),
|
||||||
name="ticket_assignee_select2_ajax",
|
name="ticket_assignee_select2_ajax",
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^problem_authors$",
|
||||||
|
ProblemAuthorSearchSelect2View.as_view(),
|
||||||
|
name="problem_authors_select2_ajax",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -910,19 +966,6 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r"^feed/",
|
|
||||||
include(
|
|
||||||
[
|
|
||||||
url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"),
|
|
||||||
url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"),
|
|
||||||
url(r"^comment/rss/$", CommentFeed(), name="comment_rss"),
|
|
||||||
url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"),
|
|
||||||
url(r"^blog/rss/$", BlogFeed(), name="blog_rss"),
|
|
||||||
url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"^stats/",
|
r"^stats/",
|
||||||
include(
|
include(
|
||||||
|
@ -1023,9 +1066,6 @@ urlpatterns = [
|
||||||
url(
|
url(
|
||||||
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
|
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1131,8 +1171,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^notifications/",
|
r"^notifications/",
|
||||||
login_required(notification.NotificationList.as_view()),
|
paged_list_view(notification.NotificationList, "notification"),
|
||||||
name="notification",
|
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^import_users/",
|
r"^import_users/",
|
||||||
|
@ -1162,6 +1201,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"),
|
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"),
|
||||||
|
url(r"^upload/$", custom_file_upload.file_upload, name="custom_file_upload"),
|
||||||
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# if hasattr(settings, "INTERNAL_IPS"):
|
# if hasattr(settings, "INTERNAL_IPS"):
|
||||||
|
|
|
@ -20,9 +20,15 @@ from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
|
||||||
from judge.admin.profile import ProfileAdmin, UserAdmin
|
from judge.admin.profile import ProfileAdmin, UserAdmin
|
||||||
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
|
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
|
||||||
from judge.admin.submission import SubmissionAdmin
|
from judge.admin.submission import SubmissionAdmin
|
||||||
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
|
from judge.admin.taxon import (
|
||||||
|
ProblemGroupAdmin,
|
||||||
|
ProblemTypeAdmin,
|
||||||
|
OfficialContestCategoryAdmin,
|
||||||
|
OfficialContestLocationAdmin,
|
||||||
|
)
|
||||||
from judge.admin.ticket import TicketAdmin
|
from judge.admin.ticket import TicketAdmin
|
||||||
from judge.admin.volunteer import VolunteerProblemVoteAdmin
|
from judge.admin.volunteer import VolunteerProblemVoteAdmin
|
||||||
|
from judge.admin.course import CourseAdmin
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
BlogPost,
|
BlogPost,
|
||||||
Comment,
|
Comment,
|
||||||
|
@ -47,6 +53,8 @@ from judge.models import (
|
||||||
VolunteerProblemVote,
|
VolunteerProblemVote,
|
||||||
Course,
|
Course,
|
||||||
ContestsSummary,
|
ContestsSummary,
|
||||||
|
OfficialContestCategory,
|
||||||
|
OfficialContestLocation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +80,9 @@ admin.site.register(Profile, ProfileAdmin)
|
||||||
admin.site.register(Submission, SubmissionAdmin)
|
admin.site.register(Submission, SubmissionAdmin)
|
||||||
admin.site.register(Ticket, TicketAdmin)
|
admin.site.register(Ticket, TicketAdmin)
|
||||||
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
||||||
admin.site.register(Course)
|
admin.site.register(Course, CourseAdmin)
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
|
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
|
||||||
|
admin.site.register(OfficialContestCategory, OfficialContestCategoryAdmin)
|
||||||
|
admin.site.register(OfficialContestLocation, OfficialContestLocationAdmin)
|
||||||
|
|
|
@ -12,7 +12,6 @@ class CommentForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
|
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
|
||||||
"parent": AdminHeavySelect2Widget(data_view="comment_select2"),
|
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
@ -39,7 +38,7 @@ class CommentAdmin(VersionAdmin):
|
||||||
)
|
)
|
||||||
list_display = ["author", "linked_object", "time"]
|
list_display = ["author", "linked_object", "time"]
|
||||||
search_fields = ["author__user__username", "body"]
|
search_fields = ["author__user__username", "body"]
|
||||||
readonly_fields = ["score"]
|
readonly_fields = ["score", "parent"]
|
||||||
actions = ["hide_comment", "unhide_comment"]
|
actions = ["hide_comment", "unhide_comment"]
|
||||||
list_filter = ["hidden"]
|
list_filter = ["hidden"]
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
|
|
|
@ -14,7 +14,14 @@ from reversion.admin import VersionAdmin
|
||||||
from reversion_compare.admin import CompareVersionAdmin
|
from reversion_compare.admin import CompareVersionAdmin
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
|
from judge.models import (
|
||||||
|
Contest,
|
||||||
|
ContestProblem,
|
||||||
|
ContestSubmission,
|
||||||
|
Profile,
|
||||||
|
Rating,
|
||||||
|
OfficialContest,
|
||||||
|
)
|
||||||
from judge.ratings import rate_contest
|
from judge.ratings import rate_contest
|
||||||
from judge.widgets import (
|
from judge.widgets import (
|
||||||
AdminHeavySelect2MultipleWidget,
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
@ -24,6 +31,7 @@ from judge.widgets import (
|
||||||
AdminSelect2Widget,
|
AdminSelect2Widget,
|
||||||
HeavyPreviewAdminPageDownWidget,
|
HeavyPreviewAdminPageDownWidget,
|
||||||
)
|
)
|
||||||
|
from judge.views.contests import recalculate_contest_summary_result
|
||||||
|
|
||||||
|
|
||||||
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||||
|
@ -148,6 +156,26 @@ class ContestForm(ModelForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestInlineForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"category": AdminSelect2Widget,
|
||||||
|
"location": AdminSelect2Widget,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestInline(admin.StackedInline):
|
||||||
|
fields = (
|
||||||
|
"category",
|
||||||
|
"year",
|
||||||
|
"location",
|
||||||
|
)
|
||||||
|
model = OfficialContest
|
||||||
|
can_delete = True
|
||||||
|
form = OfficialContestInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ContestAdmin(CompareVersionAdmin):
|
class ContestAdmin(CompareVersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
|
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
|
||||||
|
@ -162,6 +190,7 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
"scoreboard_visibility",
|
"scoreboard_visibility",
|
||||||
"run_pretests_only",
|
"run_pretests_only",
|
||||||
"points_precision",
|
"points_precision",
|
||||||
|
"rate_limit",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -221,7 +250,7 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
"user_count",
|
"user_count",
|
||||||
)
|
)
|
||||||
search_fields = ("key", "name")
|
search_fields = ("key", "name")
|
||||||
inlines = [ContestProblemInline]
|
inlines = [ContestProblemInline, OfficialContestInline]
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ContestForm
|
form = ContestForm
|
||||||
|
@ -297,15 +326,23 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
self._rescore(obj.key)
|
self._rescore(obj.key)
|
||||||
self._rescored = True
|
self._rescored = True
|
||||||
|
|
||||||
|
if form.changed_data and any(
|
||||||
|
f in form.changed_data
|
||||||
|
for f in (
|
||||||
|
"authors",
|
||||||
|
"curators",
|
||||||
|
"testers",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
Contest._author_ids.dirty(obj)
|
||||||
|
Contest._curator_ids.dirty(obj)
|
||||||
|
Contest._tester_ids.dirty(obj)
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
def save_related(self, request, form, formsets, change):
|
||||||
super().save_related(request, form, formsets, change)
|
super().save_related(request, form, formsets, change)
|
||||||
# Only rescored if we did not already do so in `save_model`
|
# Only rescored if we did not already do so in `save_model`
|
||||||
if not self._rescored and any(formset.has_changed() for formset in formsets):
|
if not self._rescored and any(formset.has_changed() for formset in formsets):
|
||||||
self._rescore(form.cleaned_data["key"])
|
self._rescore(form.cleaned_data["key"])
|
||||||
obj = form.instance
|
|
||||||
obj.is_organization_private = obj.organizations.count() > 0
|
|
||||||
obj.is_private = obj.private_contestants.count() > 0
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not request.user.has_perm("judge.edit_own_contest"):
|
if not request.user.has_perm("judge.edit_own_contest"):
|
||||||
|
@ -518,3 +555,9 @@ class ContestsSummaryAdmin(admin.ModelAdmin):
|
||||||
list_display = ("key",)
|
list_display = ("key",)
|
||||||
search_fields = ("key", "contests__key")
|
search_fields = ("key", "contests__key")
|
||||||
form = ContestsSummaryForm
|
form = ContestsSummaryForm
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
|
||||||
|
obj.refresh_from_db()
|
||||||
|
obj.results = recalculate_contest_summary_result(obj)
|
||||||
|
obj.save()
|
||||||
|
|
52
judge/admin/course.py
Normal file
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,7 +53,8 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||||
class BlogPostForm(ModelForm):
|
class BlogPostForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(BlogPostForm, self).__init__(*args, **kwargs)
|
super(BlogPostForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["authors"].widget.can_add_related = False
|
if "authors" in self.fields:
|
||||||
|
self.fields["authors"].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin, messages
|
||||||
from django.db import transaction
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import Q, Avg, Count
|
from django.db.models import Q, Avg, Count
|
||||||
from django.db.models.aggregates import StdDev
|
from django.db.models.aggregates import StdDev
|
||||||
from django.forms import ModelForm, TextInput
|
from django.forms import ModelForm, TextInput
|
||||||
|
@ -11,6 +11,7 @@ from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
from reversion_compare.admin import CompareVersionAdmin
|
from reversion_compare.admin import CompareVersionAdmin
|
||||||
|
@ -56,6 +57,16 @@ class ProblemForm(ModelForm):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean_code(self):
|
||||||
|
code = self.cleaned_data.get("code")
|
||||||
|
if self.instance.pk:
|
||||||
|
return code
|
||||||
|
|
||||||
|
if Problem.objects.filter(code=code).exists():
|
||||||
|
raise ValidationError(_("A problem with this code already exists."))
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
memory_unit = self.cleaned_data.get("memory_unit", "KB")
|
memory_unit = self.cleaned_data.get("memory_unit", "KB")
|
||||||
if memory_unit == "MB":
|
if memory_unit == "MB":
|
||||||
|
@ -131,6 +142,7 @@ class LanguageLimitInline(admin.TabularInline):
|
||||||
model = LanguageLimit
|
model = LanguageLimit
|
||||||
fields = ("language", "time_limit", "memory_limit", "memory_unit")
|
fields = ("language", "time_limit", "memory_limit", "memory_unit")
|
||||||
form = LanguageLimitInlineForm
|
form = LanguageLimitInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class LanguageTemplateInlineForm(ModelForm):
|
class LanguageTemplateInlineForm(ModelForm):
|
||||||
|
@ -145,6 +157,7 @@ class LanguageTemplateInline(admin.TabularInline):
|
||||||
model = LanguageTemplate
|
model = LanguageTemplate
|
||||||
fields = ("language", "source")
|
fields = ("language", "source")
|
||||||
form = LanguageTemplateInlineForm
|
form = LanguageTemplateInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemSolutionForm(ModelForm):
|
class ProblemSolutionForm(ModelForm):
|
||||||
|
@ -370,8 +383,6 @@ class ProblemAdmin(CompareVersionAdmin):
|
||||||
super().save_related(request, form, formsets, change)
|
super().save_related(request, form, formsets, change)
|
||||||
obj = form.instance
|
obj = form.instance
|
||||||
obj.curators.add(request.profile)
|
obj.curators.add(request.profile)
|
||||||
obj.is_organization_private = obj.organizations.count() > 0
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
if "curators" in form.changed_data or "authors" in form.changed_data:
|
if "curators" in form.changed_data or "authors" in form.changed_data:
|
||||||
del obj.editor_ids
|
del obj.editor_ids
|
||||||
|
|
|
@ -6,7 +6,7 @@ from reversion.admin import VersionAdmin
|
||||||
from django.contrib.auth.admin import UserAdmin as OldUserAdmin
|
from django.contrib.auth.admin import UserAdmin as OldUserAdmin
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import Profile
|
from judge.models import Profile, ProfileInfo
|
||||||
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,6 +54,13 @@ class TimezoneFilter(admin.SimpleListFilter):
|
||||||
return queryset.filter(timezone=self.value())
|
return queryset.filter(timezone=self.value())
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileInfoInline(admin.StackedInline):
|
||||||
|
model = ProfileInfo
|
||||||
|
can_delete = False
|
||||||
|
verbose_name_plural = "profile info"
|
||||||
|
fk_name = "profile"
|
||||||
|
|
||||||
|
|
||||||
class ProfileAdmin(VersionAdmin):
|
class ProfileAdmin(VersionAdmin):
|
||||||
fields = (
|
fields = (
|
||||||
"user",
|
"user",
|
||||||
|
@ -63,15 +70,12 @@ class ProfileAdmin(VersionAdmin):
|
||||||
"timezone",
|
"timezone",
|
||||||
"language",
|
"language",
|
||||||
"ace_theme",
|
"ace_theme",
|
||||||
"math_engine",
|
|
||||||
"last_access",
|
"last_access",
|
||||||
"ip",
|
"ip",
|
||||||
"mute",
|
"mute",
|
||||||
"is_unlisted",
|
"is_unlisted",
|
||||||
"is_banned_problem_voting",
|
|
||||||
"notes",
|
"notes",
|
||||||
"is_totp_enabled",
|
"is_totp_enabled",
|
||||||
"user_script",
|
|
||||||
"current_contest",
|
"current_contest",
|
||||||
)
|
)
|
||||||
readonly_fields = ("user",)
|
readonly_fields = ("user",)
|
||||||
|
@ -92,6 +96,7 @@ class ProfileAdmin(VersionAdmin):
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ProfileForm
|
form = ProfileForm
|
||||||
|
inlines = (ProfileInfoInline,)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super(ProfileAdmin, self).get_queryset(request).select_related("user")
|
return super(ProfileAdmin, self).get_queryset(request).select_related("user")
|
||||||
|
@ -160,15 +165,6 @@ class ProfileAdmin(VersionAdmin):
|
||||||
|
|
||||||
recalculate_points.short_description = _("Recalculate scores")
|
recalculate_points.short_description = _("Recalculate scores")
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
|
||||||
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
|
|
||||||
if "user_script" in form.base_fields:
|
|
||||||
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
|
|
||||||
form.base_fields["user_script"].widget = AceWidget(
|
|
||||||
"javascript", request.profile.ace_theme
|
|
||||||
)
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(OldUserAdmin):
|
class UserAdmin(OldUserAdmin):
|
||||||
# Customize the fieldsets for adding and editing users
|
# Customize the fieldsets for adding and editing users
|
||||||
|
|
|
@ -56,3 +56,11 @@ class ProblemTypeAdmin(admin.ModelAdmin):
|
||||||
[o.pk for o in obj.problem_set.all()] if obj else []
|
[o.pk for o in obj.problem_set.all()] if obj else []
|
||||||
)
|
)
|
||||||
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
|
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestCategoryAdmin(admin.ModelAdmin):
|
||||||
|
fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestLocationAdmin(admin.ModelAdmin):
|
||||||
|
fields = ("name",)
|
||||||
|
|
|
@ -12,7 +12,7 @@ class JudgeAppConfig(AppConfig):
|
||||||
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
||||||
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from . import signals, jinja2 # noqa: F401, imported for side effects
|
from . import models, signals, jinja2 # noqa: F401, imported for side effects
|
||||||
|
|
||||||
from django.contrib.flatpages.models import FlatPage
|
from django.contrib.flatpages.models import FlatPage
|
||||||
from django.contrib.flatpages.admin import FlatPageAdmin
|
from django.contrib.flatpages.admin import FlatPageAdmin
|
||||||
|
|
48
judge/authentication.py
Normal file
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,
|
Submission,
|
||||||
SubmissionTestCase,
|
SubmissionTestCase,
|
||||||
)
|
)
|
||||||
|
from judge.bridge.utils import VanishedSubmission
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
logger = logging.getLogger("judge.bridge")
|
||||||
json_log = logging.getLogger("judge.json.bridge")
|
json_log = logging.getLogger("judge.json.bridge")
|
||||||
|
@ -65,9 +67,8 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
self._working = False
|
self._working = False
|
||||||
self._working_data = {}
|
self._working_data = {}
|
||||||
self._no_response_job = None
|
self._no_response_job = None
|
||||||
self._problems = []
|
|
||||||
self.executors = {}
|
self.executors = {}
|
||||||
self.problems = {}
|
self.problems = set()
|
||||||
self.latency = None
|
self.latency = None
|
||||||
self.time_delta = None
|
self.time_delta = None
|
||||||
self.load = 1e100
|
self.load = 1e100
|
||||||
|
@ -139,11 +140,52 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _update_supported_problems(self, problem_packet):
|
||||||
|
# problem_packet is a dict {code: mtimes} from judge-server
|
||||||
|
self.problems = set(p for p, _ in problem_packet)
|
||||||
|
|
||||||
|
def _update_judge_problems(self):
|
||||||
|
chunk_size = 500
|
||||||
|
|
||||||
|
target_problem_codes = self.problems
|
||||||
|
current_problems = _get_judge_problems(self.judge)
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
problems_to_add = list(target_problem_codes - current_problems)
|
||||||
|
problems_to_remove = list(current_problems - target_problem_codes)
|
||||||
|
|
||||||
|
if problems_to_add:
|
||||||
|
for i in range(0, len(problems_to_add), chunk_size):
|
||||||
|
chunk = problems_to_add[i : i + chunk_size]
|
||||||
|
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
|
||||||
|
"id", flat=True
|
||||||
|
)
|
||||||
|
if not problem_ids:
|
||||||
|
continue
|
||||||
|
logger.info("%s: Add %d problems", self.name, len(problem_ids))
|
||||||
|
self.judge.problems.add(*problem_ids)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if problems_to_remove:
|
||||||
|
for i in range(0, len(problems_to_remove), chunk_size):
|
||||||
|
chunk = problems_to_remove[i : i + chunk_size]
|
||||||
|
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
|
||||||
|
"id", flat=True
|
||||||
|
)
|
||||||
|
if not problem_ids:
|
||||||
|
continue
|
||||||
|
logger.info("%s: Remove %d problems", self.name, len(problem_ids))
|
||||||
|
self.judge.problems.remove(*problem_ids)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
_get_judge_problems.dirty(self.judge)
|
||||||
|
|
||||||
def _connected(self):
|
def _connected(self):
|
||||||
judge = self.judge = Judge.objects.get(name=self.name)
|
judge = self.judge = Judge.objects.get(name=self.name)
|
||||||
judge.start_time = timezone.now()
|
judge.start_time = timezone.now()
|
||||||
judge.online = True
|
judge.online = True
|
||||||
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
self._update_judge_problems()
|
||||||
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
|
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
|
||||||
|
|
||||||
# Delete now in case we somehow crashed and left some over from the last connection
|
# Delete now in case we somehow crashed and left some over from the last connection
|
||||||
|
@ -178,6 +220,8 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
def _disconnected(self):
|
def _disconnected(self):
|
||||||
Judge.objects.filter(id=self.judge.id).update(online=False)
|
Judge.objects.filter(id=self.judge.id).update(online=False)
|
||||||
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
||||||
|
self.judge.problems.clear()
|
||||||
|
_get_judge_problems.dirty(self.judge)
|
||||||
|
|
||||||
def _update_ping(self):
|
def _update_ping(self):
|
||||||
try:
|
try:
|
||||||
|
@ -208,8 +252,7 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.timeout = 60
|
self.timeout = 60
|
||||||
self._problems = packet["problems"]
|
self._update_supported_problems(packet["problems"])
|
||||||
self.problems = dict(self._problems)
|
|
||||||
self.executors = packet["executors"]
|
self.executors = packet["executors"]
|
||||||
self.name = packet["id"]
|
self.name = packet["id"]
|
||||||
|
|
||||||
|
@ -310,6 +353,9 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def submit(self, id, problem, language, source):
|
def submit(self, id, problem, language, source):
|
||||||
data = self.get_related_submission_data(id)
|
data = self.get_related_submission_data(id)
|
||||||
|
if not data:
|
||||||
|
self._update_internal_error_submission(id, "Submission vanished")
|
||||||
|
raise VanishedSubmission()
|
||||||
self._working = id
|
self._working = id
|
||||||
self._working_data = {
|
self._working_data = {
|
||||||
"problem": problem,
|
"problem": problem,
|
||||||
|
@ -434,14 +480,12 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def on_supported_problems(self, packet):
|
def on_supported_problems(self, packet):
|
||||||
logger.info("%s: Updated problem list", self.name)
|
logger.info("%s: Updated problem list", self.name)
|
||||||
self._problems = packet["problems"]
|
self._update_supported_problems(packet["problems"])
|
||||||
self.problems = dict(self._problems)
|
|
||||||
if not self.working:
|
if not self.working:
|
||||||
self.judges.update_problems(self)
|
self.judges.update_problems(self)
|
||||||
|
|
||||||
self.judge.problems.set(
|
self._update_judge_problems()
|
||||||
Problem.objects.filter(code__in=list(self.problems.keys()))
|
|
||||||
)
|
|
||||||
json_log.info(
|
json_log.info(
|
||||||
self._make_json_log(action="update-problems", count=len(self.problems))
|
self._make_json_log(action="update-problems", count=len(self.problems))
|
||||||
)
|
)
|
||||||
|
@ -658,8 +702,11 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
self._free_self(packet)
|
self._free_self(packet)
|
||||||
|
|
||||||
id = packet["submission-id"]
|
id = packet["submission-id"]
|
||||||
|
self._update_internal_error_submission(id, packet["message"])
|
||||||
|
|
||||||
|
def _update_internal_error_submission(self, id, message):
|
||||||
if Submission.objects.filter(id=id).update(
|
if Submission.objects.filter(id=id).update(
|
||||||
status="IE", result="IE", error=packet["message"]
|
status="IE", result="IE", error=message
|
||||||
):
|
):
|
||||||
event.post(
|
event.post(
|
||||||
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
|
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
|
||||||
|
@ -667,9 +714,9 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
self._post_update_submission(id, "internal-error", done=True)
|
self._post_update_submission(id, "internal-error", done=True)
|
||||||
json_log.info(
|
json_log.info(
|
||||||
self._make_json_log(
|
self._make_json_log(
|
||||||
packet,
|
sub=id,
|
||||||
action="internal-error",
|
action="internal-error",
|
||||||
message=packet["message"],
|
message=message,
|
||||||
finish=True,
|
finish=True,
|
||||||
result="IE",
|
result="IE",
|
||||||
)
|
)
|
||||||
|
@ -678,10 +725,10 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
logger.warning("Unknown submission: %s", id)
|
logger.warning("Unknown submission: %s", id)
|
||||||
json_log.error(
|
json_log.error(
|
||||||
self._make_json_log(
|
self._make_json_log(
|
||||||
packet,
|
sub=id,
|
||||||
action="internal-error",
|
action="internal-error",
|
||||||
info="unknown submission",
|
info="unknown submission",
|
||||||
message=packet["message"],
|
message=message,
|
||||||
finish=True,
|
finish=True,
|
||||||
result="IE",
|
result="IE",
|
||||||
)
|
)
|
||||||
|
@ -912,3 +959,8 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def on_cleanup(self):
|
def on_cleanup(self):
|
||||||
db.connection.close()
|
db.connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gjp", timeout=3600)
|
||||||
|
def _get_judge_problems(judge):
|
||||||
|
return set(judge.problems.values_list("code", flat=True))
|
||||||
|
|
|
@ -3,6 +3,8 @@ from collections import namedtuple
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
|
from judge.bridge.utils import VanishedSubmission
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from llist import dllist
|
from llist import dllist
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -39,6 +41,8 @@ class JudgeList(object):
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
judge.submit(id, problem, language, source)
|
judge.submit(id, problem, language, source)
|
||||||
|
except VanishedSubmission:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to dispatch %d (%s, %s) to %s",
|
"Failed to dispatch %d (%s, %s) to %s",
|
||||||
|
|
2
judge/bridge/utils.py
Normal file
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
|
import hashlib
|
||||||
|
|
||||||
|
from judge.logging import log_debug
|
||||||
|
|
||||||
MAX_NUM_CHAR = 50
|
MAX_NUM_CHAR = 50
|
||||||
NONE_RESULT = "__None__"
|
NONE_RESULT = "__None__"
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ def filter_args(args_list):
|
||||||
l0_cache = caches["l0"] if "l0" in caches else None
|
l0_cache = caches["l0"] if "l0" in caches else None
|
||||||
|
|
||||||
|
|
||||||
def cache_wrapper(prefix, timeout=None):
|
def cache_wrapper(prefix, timeout=None, expected_type=None):
|
||||||
def get_key(func, *args, **kwargs):
|
def get_key(func, *args, **kwargs):
|
||||||
args_list = list(args)
|
args_list = list(args)
|
||||||
signature_args = list(signature(func).parameters.keys())
|
signature_args = list(signature(func).parameters.keys())
|
||||||
|
@ -40,7 +42,10 @@ def cache_wrapper(prefix, timeout=None):
|
||||||
def _get(key):
|
def _get(key):
|
||||||
if not l0_cache:
|
if not l0_cache:
|
||||||
return cache.get(key)
|
return cache.get(key)
|
||||||
return l0_cache.get(key) or cache.get(key)
|
result = l0_cache.get(key)
|
||||||
|
if result is None:
|
||||||
|
result = cache.get(key)
|
||||||
|
return result
|
||||||
|
|
||||||
def _set_l0(key, value):
|
def _set_l0(key, value):
|
||||||
if l0_cache:
|
if l0_cache:
|
||||||
|
@ -51,18 +56,33 @@ def cache_wrapper(prefix, timeout=None):
|
||||||
cache.set(key, value, timeout)
|
cache.set(key, value, timeout)
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
def _validate_type(cache_key, result):
|
||||||
|
if expected_type and not isinstance(result, expected_type):
|
||||||
|
data = {
|
||||||
|
"function": f"{func.__module__}.{func.__qualname__}",
|
||||||
|
"result": str(result)[:30],
|
||||||
|
"expected_type": expected_type,
|
||||||
|
"type": type(result),
|
||||||
|
"key": cache_key,
|
||||||
|
}
|
||||||
|
log_debug("invalid_key", data)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
cache_key = get_key(func, *args, **kwargs)
|
cache_key = get_key(func, *args, **kwargs)
|
||||||
result = _get(cache_key)
|
result = _get(cache_key)
|
||||||
if result is not None:
|
if result is not None and _validate_type(cache_key, result):
|
||||||
_set_l0(cache_key, result)
|
_set_l0(cache_key, result)
|
||||||
if result == NONE_RESULT:
|
if type(result) == str and result == NONE_RESULT:
|
||||||
result = None
|
result = None
|
||||||
return result
|
return result
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
if result is None:
|
if result is None:
|
||||||
result = NONE_RESULT
|
cache_result = NONE_RESULT
|
||||||
_set(cache_key, result, timeout)
|
else:
|
||||||
|
cache_result = result
|
||||||
|
_set(cache_key, cache_result, timeout)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def dirty(*args, **kwargs):
|
def dirty(*args, **kwargs):
|
||||||
|
@ -71,7 +91,26 @@ def cache_wrapper(prefix, timeout=None):
|
||||||
if l0_cache:
|
if l0_cache:
|
||||||
l0_cache.delete(cache_key)
|
l0_cache.delete(cache_key)
|
||||||
|
|
||||||
|
def prefetch_multi(args_list):
|
||||||
|
keys = []
|
||||||
|
for args in args_list:
|
||||||
|
keys.append(get_key(func, *args))
|
||||||
|
results = cache.get_many(keys)
|
||||||
|
for key, result in results.items():
|
||||||
|
if result is not None:
|
||||||
|
_set_l0(key, result)
|
||||||
|
|
||||||
|
def dirty_multi(args_list):
|
||||||
|
keys = []
|
||||||
|
for args in args_list:
|
||||||
|
keys.append(get_key(func, *args))
|
||||||
|
cache.delete_many(keys)
|
||||||
|
if l0_cache:
|
||||||
|
l0_cache.delete_many(keys)
|
||||||
|
|
||||||
wrapper.dirty = dirty
|
wrapper.dirty = dirty
|
||||||
|
wrapper.prefetch_multi = prefetch_multi
|
||||||
|
wrapper.dirty_multi = dirty_multi
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
|
@ -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:
|
for result in queryset:
|
||||||
problem = str(result["problem_id"])
|
problem = str(result["problem_id"])
|
||||||
|
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
|
||||||
|
continue
|
||||||
if format_data.get(problem):
|
if format_data.get(problem):
|
||||||
is_after_freeze = (
|
is_after_freeze = (
|
||||||
self.contest.freeze_after
|
self.contest.freeze_after
|
||||||
|
|
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",
|
"ip": "10.0.2.2",
|
||||||
"language": 1,
|
"language": 1,
|
||||||
"last_access": "2017-12-02T08:57:10.093Z",
|
"last_access": "2017-12-02T08:57:10.093Z",
|
||||||
"math_engine": "auto",
|
|
||||||
"mute": false,
|
"mute": false,
|
||||||
"organizations": [
|
"organizations": [
|
||||||
1
|
1
|
||||||
|
@ -18,8 +17,7 @@
|
||||||
"problem_count": 0,
|
"problem_count": 0,
|
||||||
"rating": null,
|
"rating": null,
|
||||||
"timezone": "America/Toronto",
|
"timezone": "America/Toronto",
|
||||||
"user": 1,
|
"user": 1
|
||||||
"user_script": ""
|
|
||||||
},
|
},
|
||||||
"model": "judge.profile",
|
"model": "judge.profile",
|
||||||
"pk": 1
|
"pk": 1
|
||||||
|
|
|
@ -29,6 +29,7 @@ from django_ace import AceWidget
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
Contest,
|
Contest,
|
||||||
Language,
|
Language,
|
||||||
|
TestFormatterModel,
|
||||||
Organization,
|
Organization,
|
||||||
PrivateMessage,
|
PrivateMessage,
|
||||||
Problem,
|
Problem,
|
||||||
|
@ -37,11 +38,12 @@ from judge.models import (
|
||||||
Submission,
|
Submission,
|
||||||
BlogPost,
|
BlogPost,
|
||||||
ContestProblem,
|
ContestProblem,
|
||||||
|
TestFormatterModel,
|
||||||
|
ProfileInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
from judge.widgets import (
|
from judge.widgets import (
|
||||||
HeavyPreviewPageDownWidget,
|
HeavyPreviewPageDownWidget,
|
||||||
MathJaxPagedownWidget,
|
|
||||||
PagedownWidget,
|
PagedownWidget,
|
||||||
Select2MultipleWidget,
|
Select2MultipleWidget,
|
||||||
Select2Widget,
|
Select2Widget,
|
||||||
|
@ -50,6 +52,7 @@ from judge.widgets import (
|
||||||
Select2MultipleWidget,
|
Select2MultipleWidget,
|
||||||
DateTimePickerWidget,
|
DateTimePickerWidget,
|
||||||
ImageWidget,
|
ImageWidget,
|
||||||
|
DatePickerWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +71,17 @@ class UserForm(ModelForm):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileInfoForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ProfileInfo
|
||||||
|
fields = ["tshirt_size", "date_of_birth", "address"]
|
||||||
|
widgets = {
|
||||||
|
"tshirt_size": Select2Widget(attrs={"style": "width:100%"}),
|
||||||
|
"date_of_birth": DatePickerWidget,
|
||||||
|
"address": forms.TextInput(attrs={"style": "width:100%"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(ModelForm):
|
class ProfileForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Profile
|
model = Profile
|
||||||
|
@ -76,12 +90,10 @@ class ProfileForm(ModelForm):
|
||||||
"timezone",
|
"timezone",
|
||||||
"language",
|
"language",
|
||||||
"ace_theme",
|
"ace_theme",
|
||||||
"user_script",
|
|
||||||
"profile_image",
|
"profile_image",
|
||||||
"css_background",
|
"css_background",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"user_script": AceWidget(theme="github"),
|
|
||||||
"timezone": Select2Widget(attrs={"style": "width:200px"}),
|
"timezone": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
"language": Select2Widget(attrs={"style": "width:200px"}),
|
"language": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
|
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
|
@ -89,11 +101,6 @@ class ProfileForm(ModelForm):
|
||||||
"css_background": forms.TextInput(),
|
"css_background": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
has_math_config = bool(settings.MATHOID_URL)
|
|
||||||
if has_math_config:
|
|
||||||
fields.append("math_engine")
|
|
||||||
widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"})
|
|
||||||
|
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
widgets["about"] = HeavyPreviewPageDownWidget(
|
widgets["about"] = HeavyPreviewPageDownWidget(
|
||||||
preview=reverse_lazy("profile_preview"),
|
preview=reverse_lazy("profile_preview"),
|
||||||
|
@ -301,8 +308,8 @@ class EditOrganizationContestForm(ModelForm):
|
||||||
"hide_problem_tags",
|
"hide_problem_tags",
|
||||||
"public_scoreboard",
|
"public_scoreboard",
|
||||||
"scoreboard_visibility",
|
"scoreboard_visibility",
|
||||||
"run_pretests_only",
|
|
||||||
"points_precision",
|
"points_precision",
|
||||||
|
"rate_limit",
|
||||||
"description",
|
"description",
|
||||||
"og_image",
|
"og_image",
|
||||||
"logo_override_image",
|
"logo_override_image",
|
||||||
|
@ -412,13 +419,15 @@ class NewMessageForm(ModelForm):
|
||||||
fields = ["title", "content"]
|
fields = ["title", "content"]
|
||||||
widgets = {}
|
widgets = {}
|
||||||
if PagedownWidget is not None:
|
if PagedownWidget is not None:
|
||||||
widgets["content"] = MathJaxPagedownWidget()
|
widgets["content"] = PagedownWidget()
|
||||||
|
|
||||||
|
|
||||||
class CustomAuthenticationForm(AuthenticationForm):
|
class CustomAuthenticationForm(AuthenticationForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
|
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["username"].widget.attrs.update({"placeholder": _("Username")})
|
self.fields["username"].widget.attrs.update(
|
||||||
|
{"placeholder": _("Username/Email")}
|
||||||
|
)
|
||||||
self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
|
self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
|
||||||
|
|
||||||
self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
|
self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
|
||||||
|
@ -566,3 +575,9 @@ class ContestProblemFormSet(
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
model = ContestProblem
|
model = ContestProblem
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatterForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = TestFormatterModel
|
||||||
|
fields = ["file"]
|
||||||
|
|
|
@ -1,44 +1,13 @@
|
||||||
from django.utils.html import escape, mark_safe
|
from django.utils.html import escape, mark_safe
|
||||||
|
from judge.markdown import markdown
|
||||||
|
|
||||||
__all__ = ["highlight_code"]
|
__all__ = ["highlight_code"]
|
||||||
|
|
||||||
|
|
||||||
def _make_pre_code(code):
|
def highlight_code(code, language, linenos=True, title=None):
|
||||||
return mark_safe("<pre>" + escape(code) + "</pre>")
|
linenos_option = 'linenums="1"' if linenos else ""
|
||||||
|
title_option = f'title="{title}"' if title else ""
|
||||||
|
options = f"{{.{language} {linenos_option} {title_option}}}"
|
||||||
|
|
||||||
|
value = f"```{options}\n{code}\n```\n"
|
||||||
try:
|
return mark_safe(markdown(value))
|
||||||
import pygments
|
|
||||||
import pygments.lexers
|
|
||||||
import pygments.formatters
|
|
||||||
import pygments.util
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
def highlight_code(code, language, cssclass=None):
|
|
||||||
return _make_pre_code(code)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def highlight_code(code, language, cssclass="codehilite", linenos=True):
|
|
||||||
try:
|
|
||||||
lexer = pygments.lexers.get_lexer_by_name(language)
|
|
||||||
except pygments.util.ClassNotFound:
|
|
||||||
return _make_pre_code(code)
|
|
||||||
|
|
||||||
if linenos:
|
|
||||||
return mark_safe(
|
|
||||||
pygments.highlight(
|
|
||||||
code,
|
|
||||||
lexer,
|
|
||||||
pygments.formatters.HtmlFormatter(
|
|
||||||
cssclass=cssclass, linenos="table", wrapcode=True
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return mark_safe(
|
|
||||||
pygments.highlight(
|
|
||||||
code,
|
|
||||||
lexer,
|
|
||||||
pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ from . import (
|
||||||
social,
|
social,
|
||||||
spaceless,
|
spaceless,
|
||||||
timedelta,
|
timedelta,
|
||||||
|
comment,
|
||||||
)
|
)
|
||||||
from . import registry
|
from . import registry
|
||||||
|
|
||||||
|
|
12
judge/jinja2/comment.py
Normal file
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.function
|
||||||
@registry.render_with("widgets/relative-time.html")
|
@registry.render_with("widgets/relative-time.html")
|
||||||
def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("on {time}")):
|
def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("{time}")):
|
||||||
return {"time": time, "format": format, "rel_format": rel, "abs_format": abs}
|
return {"time": time, "format": format, "rel_format": rel, "abs_format": abs}
|
||||||
|
|
|
@ -10,10 +10,11 @@ from . import registry
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
def gravatar(profile, size=80, default=None, profile_image=None, email=None):
|
def gravatar(profile, size=80, default=None, profile_image=None, email=None):
|
||||||
if profile_image:
|
if profile and not profile.is_muted:
|
||||||
return profile_image
|
if profile_image:
|
||||||
if profile and profile.profile_image_url:
|
return profile_image
|
||||||
return profile.profile_image_url
|
if profile and profile.profile_image_url:
|
||||||
|
return profile.profile_image_url
|
||||||
if profile:
|
if profile:
|
||||||
email = email or profile.email
|
email = email or profile.email
|
||||||
if default is None:
|
if default is None:
|
||||||
|
|
|
@ -1,112 +1,7 @@
|
||||||
from .. import registry
|
from .. import registry
|
||||||
import markdown as _markdown
|
from judge.markdown import markdown as _markdown
|
||||||
import bleach
|
|
||||||
from django.utils.html import escape
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from pymdownx import superfences
|
|
||||||
|
|
||||||
|
|
||||||
EXTENSIONS = [
|
|
||||||
"pymdownx.arithmatex",
|
|
||||||
"pymdownx.magiclink",
|
|
||||||
"pymdownx.betterem",
|
|
||||||
"pymdownx.details",
|
|
||||||
"pymdownx.emoji",
|
|
||||||
"pymdownx.inlinehilite",
|
|
||||||
"pymdownx.superfences",
|
|
||||||
"pymdownx.tasklist",
|
|
||||||
"markdown.extensions.footnotes",
|
|
||||||
"markdown.extensions.attr_list",
|
|
||||||
"markdown.extensions.def_list",
|
|
||||||
"markdown.extensions.tables",
|
|
||||||
"markdown.extensions.admonition",
|
|
||||||
"nl2br",
|
|
||||||
"mdx_breakless_lists",
|
|
||||||
]
|
|
||||||
|
|
||||||
EXTENSION_CONFIGS = {
|
|
||||||
"pymdownx.superfences": {
|
|
||||||
"custom_fences": [
|
|
||||||
{
|
|
||||||
"name": "sample",
|
|
||||||
"class": "no-border",
|
|
||||||
"format": superfences.fence_code_format,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
|
||||||
"img",
|
|
||||||
"center",
|
|
||||||
"iframe",
|
|
||||||
"div",
|
|
||||||
"span",
|
|
||||||
"table",
|
|
||||||
"tr",
|
|
||||||
"td",
|
|
||||||
"th",
|
|
||||||
"tr",
|
|
||||||
"pre",
|
|
||||||
"code",
|
|
||||||
"p",
|
|
||||||
"hr",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
"h3",
|
|
||||||
"h4",
|
|
||||||
"h5",
|
|
||||||
"h6",
|
|
||||||
"thead",
|
|
||||||
"tbody",
|
|
||||||
"sup",
|
|
||||||
"dl",
|
|
||||||
"dt",
|
|
||||||
"dd",
|
|
||||||
"br",
|
|
||||||
"details",
|
|
||||||
"summary",
|
|
||||||
]
|
|
||||||
|
|
||||||
ALLOWED_ATTRS = ["src", "width", "height", "href", "class", "open"]
|
|
||||||
|
|
||||||
|
|
||||||
@registry.filter
|
@registry.filter
|
||||||
def markdown(value, lazy_load=False):
|
def markdown(value, lazy_load=False):
|
||||||
extensions = EXTENSIONS
|
return _markdown(value, lazy_load)
|
||||||
html = _markdown.markdown(
|
|
||||||
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
|
|
||||||
)
|
|
||||||
|
|
||||||
# Don't clean mathjax
|
|
||||||
hash_script_tag = {}
|
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
|
||||||
for script_tag in soup.find_all("script"):
|
|
||||||
allow_math_types = ["math/tex", "math/tex; mode=display"]
|
|
||||||
if script_tag.attrs.get("type", False) in allow_math_types:
|
|
||||||
hash_script_tag[str(hash(str(script_tag)))] = str(script_tag)
|
|
||||||
|
|
||||||
for hashed_tag in hash_script_tag:
|
|
||||||
tag = hash_script_tag[hashed_tag]
|
|
||||||
html = html.replace(tag, hashed_tag)
|
|
||||||
|
|
||||||
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
|
|
||||||
|
|
||||||
for hashed_tag in hash_script_tag:
|
|
||||||
tag = hash_script_tag[hashed_tag]
|
|
||||||
html = html.replace(hashed_tag, tag)
|
|
||||||
|
|
||||||
if not html:
|
|
||||||
html = escape(value)
|
|
||||||
if lazy_load:
|
|
||||||
soup = BeautifulSoup(html, features="html.parser")
|
|
||||||
for img in soup.findAll("img"):
|
|
||||||
if img.get("src"):
|
|
||||||
img["data-src"] = img["src"]
|
|
||||||
img["src"] = ""
|
|
||||||
for img in soup.findAll("iframe"):
|
|
||||||
if img.get("src"):
|
|
||||||
img["data-src"] = img["src"]
|
|
||||||
img["src"] = ""
|
|
||||||
html = str(soup)
|
|
||||||
return '<div class="md-typeset">%s</div>' % html
|
|
||||||
|
|
|
@ -155,16 +155,16 @@ def item_title(item):
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
@registry.render_with("user/link.html")
|
@registry.render_with("user/link.html")
|
||||||
def link_user(user):
|
def link_user(user, show_image=False):
|
||||||
if isinstance(user, Profile):
|
if isinstance(user, Profile):
|
||||||
profile = user
|
profile = user
|
||||||
elif isinstance(user, AbstractUser):
|
elif isinstance(user, AbstractUser):
|
||||||
profile = user.profile
|
profile = user.profile
|
||||||
elif type(user).__name__ == "ContestRankingProfile":
|
elif isinstance(user, int):
|
||||||
profile = user
|
profile = Profile(id=user)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Expected profile or user, got %s" % (type(user),))
|
raise ValueError("Expected profile or user, got %s" % (type(user),))
|
||||||
return {"profile": profile}
|
return {"profile": profile, "show_image": show_image}
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
|
|
|
@ -48,5 +48,9 @@ for name, template, url_func in SHARES:
|
||||||
@registry.function
|
@registry.function
|
||||||
def recaptcha_init(language=None):
|
def recaptcha_init(language=None):
|
||||||
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
|
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
|
||||||
{"explicit": False, "language": language}
|
{
|
||||||
|
"explicit": False,
|
||||||
|
"language": language,
|
||||||
|
"recaptcha_host": "https://google.com",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
error_log = logging.getLogger("judge.errors")
|
error_log = logging.getLogger("judge.errors")
|
||||||
|
debug_log = logging.getLogger("judge.debug")
|
||||||
|
|
||||||
|
|
||||||
def log_exception(msg):
|
def log_exception(msg):
|
||||||
error_log.exception(msg)
|
error_log.exception(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def log_debug(category, data):
|
||||||
|
debug_log.info(f"{category}: {data}")
|
||||||
|
|
|
@ -89,14 +89,13 @@ class Command(BaseCommand):
|
||||||
if trans is None
|
if trans is None
|
||||||
else trans.description,
|
else trans.description,
|
||||||
"url": "",
|
"url": "",
|
||||||
"math_engine": maker.math_engine,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.replace('"//', '"https://')
|
.replace('"//', '"https://')
|
||||||
.replace("'//", "'https://")
|
.replace("'//", "'https://")
|
||||||
)
|
)
|
||||||
maker.title = problem_name
|
maker.title = problem_name
|
||||||
for file in ("style.css", "pygment-github.css", "mathjax3_config.js"):
|
for file in "style.css":
|
||||||
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
||||||
maker.make(debug=True)
|
maker.make(debug=True)
|
||||||
if not maker.success:
|
if not maker.success:
|
||||||
|
|
149
judge/markdown.py
Normal file
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
|
import numpy as np
|
||||||
from django.conf import settings
|
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from judge.caching import cache_wrapper
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
@ -12,67 +14,69 @@ class CollabFilter:
|
||||||
|
|
||||||
# name = 'collab_filter' or 'collab_filter_time'
|
# name = 'collab_filter' or 'collab_filter_time'
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
embeddings = np.load(
|
self.embeddings = np.load(
|
||||||
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
|
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
|
||||||
allow_pickle=True,
|
allow_pickle=True,
|
||||||
)
|
)
|
||||||
arr0, arr1 = embeddings.files
|
_, problem_arr = self.embeddings.files
|
||||||
self.name = name
|
self.name = name
|
||||||
self.user_embeddings = embeddings[arr0]
|
self.problem_embeddings = self.embeddings[problem_arr].item()
|
||||||
self.problem_embeddings = embeddings[arr1]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
|
def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
|
||||||
"""Computes the scores of the candidates given a query.
|
"""Return {id: score}"""
|
||||||
Args:
|
|
||||||
query_embedding: a vector of shape [k], representing the query embedding.
|
|
||||||
item_embeddings: a matrix of shape [N, k], such that row i is the embedding
|
|
||||||
of item i.
|
|
||||||
measure: a string specifying the similarity measure to be used. Can be
|
|
||||||
either DOT or COSINE.
|
|
||||||
Returns:
|
|
||||||
scores: a vector of shape [N], such that scores[i] is the score of item i.
|
|
||||||
"""
|
|
||||||
u = query_embedding
|
u = query_embedding
|
||||||
V = item_embeddings
|
V = np.stack(list(item_embeddings.values()))
|
||||||
if measure == self.COSINE:
|
if measure == self.COSINE:
|
||||||
V = V / np.linalg.norm(V, axis=1, keepdims=True)
|
V = V / np.linalg.norm(V, axis=1, keepdims=True)
|
||||||
u = u / np.linalg.norm(u)
|
u = u / np.linalg.norm(u)
|
||||||
scores = u.dot(V.T)
|
scores = u.dot(V.T)
|
||||||
return scores
|
scores_by_id = {id_: s for id_, s in zip(item_embeddings.keys(), scores)}
|
||||||
|
return scores_by_id
|
||||||
|
|
||||||
|
def _get_embedding_version(self):
|
||||||
|
first_problem = self.problem_embeddings[0]
|
||||||
|
array_bytes = first_problem.tobytes()
|
||||||
|
hash_object = hashlib.sha256(array_bytes)
|
||||||
|
hash_bytes = hash_object.digest()
|
||||||
|
return hash_bytes.hex()[:5]
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="CFgue", timeout=86400)
|
||||||
|
def _get_user_embedding(self, user_id, embedding_version):
|
||||||
|
user_arr, _ = self.embeddings.files
|
||||||
|
user_embeddings = self.embeddings[user_arr].item()
|
||||||
|
if user_id not in user_embeddings:
|
||||||
|
return user_embeddings[0]
|
||||||
|
return user_embeddings[user_id]
|
||||||
|
|
||||||
|
def get_user_embedding(self, user_id):
|
||||||
|
version = self._get_embedding_version()
|
||||||
|
return self._get_user_embedding(user_id, version)
|
||||||
|
|
||||||
@cache_wrapper(prefix="user_recommendations", timeout=3600)
|
@cache_wrapper(prefix="user_recommendations", timeout=3600)
|
||||||
def user_recommendations(self, user, problems, measure=DOT, limit=None):
|
def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
|
||||||
uid = user.id
|
user_embedding = self.get_user_embedding(user_id)
|
||||||
if uid >= len(self.user_embeddings):
|
scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
|
||||||
uid = 0
|
|
||||||
scores = self.compute_scores(
|
|
||||||
self.user_embeddings[uid], self.problem_embeddings, measure
|
|
||||||
)
|
|
||||||
|
|
||||||
res = [] # [(score, problem)]
|
res = [] # [(score, problem)]
|
||||||
for pid in problems:
|
for pid in problems:
|
||||||
# pid = problem.id
|
if pid in scores:
|
||||||
if pid < len(scores):
|
|
||||||
res.append((scores[pid], pid))
|
res.append((scores[pid], pid))
|
||||||
|
|
||||||
res.sort(reverse=True, key=lambda x: x[0])
|
res.sort(reverse=True, key=lambda x: x[0])
|
||||||
res = res[:limit]
|
return res[:limit]
|
||||||
return res
|
|
||||||
|
|
||||||
# return a list of pid
|
# return a list of pid
|
||||||
def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
|
def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
|
||||||
pid = problem.id
|
pid = problem.id
|
||||||
if pid >= len(self.problem_embeddings):
|
if pid not in self.problem_embeddings:
|
||||||
return []
|
return []
|
||||||
scores = self.compute_scores(
|
embedding = self.problem_embeddings[pid]
|
||||||
self.problem_embeddings[pid], self.problem_embeddings, measure
|
scores = self.compute_scores(embedding, self.problem_embeddings, measure)
|
||||||
)
|
|
||||||
res = []
|
res = []
|
||||||
for p in problemset:
|
for p in problemset:
|
||||||
if p < len(scores):
|
if p in scores:
|
||||||
res.append((scores[p], p))
|
res.append((scores[p], p))
|
||||||
res.sort(reverse=True, key=lambda x: x[0])
|
res.sort(reverse=True, key=lambda x: x[0])
|
||||||
return res[:limit]
|
return res[:limit]
|
||||||
|
|
|
@ -2,8 +2,6 @@ from reversion import revisions
|
||||||
|
|
||||||
from judge.models.choices import (
|
from judge.models.choices import (
|
||||||
ACE_THEMES,
|
ACE_THEMES,
|
||||||
EFFECTIVE_MATH_ENGINES,
|
|
||||||
MATH_ENGINES_CHOICES,
|
|
||||||
TIMEZONE,
|
TIMEZONE,
|
||||||
)
|
)
|
||||||
from judge.models.comment import Comment, CommentLock, CommentVote
|
from judge.models.comment import Comment, CommentLock, CommentVote
|
||||||
|
@ -17,6 +15,9 @@ from judge.models.contest import (
|
||||||
Rating,
|
Rating,
|
||||||
ContestProblemClarification,
|
ContestProblemClarification,
|
||||||
ContestsSummary,
|
ContestsSummary,
|
||||||
|
OfficialContestCategory,
|
||||||
|
OfficialContestLocation,
|
||||||
|
OfficialContest,
|
||||||
)
|
)
|
||||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||||
|
@ -45,6 +46,7 @@ from judge.models.profile import (
|
||||||
Profile,
|
Profile,
|
||||||
Friend,
|
Friend,
|
||||||
OrganizationProfile,
|
OrganizationProfile,
|
||||||
|
ProfileInfo,
|
||||||
)
|
)
|
||||||
from judge.models.runtime import Judge, Language, RuntimeVersion
|
from judge.models.runtime import Judge, Language, RuntimeVersion
|
||||||
from judge.models.submission import (
|
from judge.models.submission import (
|
||||||
|
@ -53,12 +55,15 @@ from judge.models.submission import (
|
||||||
SubmissionSource,
|
SubmissionSource,
|
||||||
SubmissionTestCase,
|
SubmissionTestCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from judge.models.test_formatter import TestFormatterModel
|
||||||
from judge.models.ticket import Ticket, TicketMessage
|
from judge.models.ticket import Ticket, TicketMessage
|
||||||
from judge.models.volunteer import VolunteerProblemVote
|
from judge.models.volunteer import VolunteerProblemVote
|
||||||
from judge.models.pagevote import PageVote, PageVoteVoter
|
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||||
from judge.models.bookmark import BookMark, MakeBookMark
|
from judge.models.bookmark import BookMark, MakeBookMark
|
||||||
from judge.models.course import Course
|
from judge.models.course import Course, CourseRole, CourseLesson
|
||||||
from judge.models.notification import Notification, NotificationProfile
|
from judge.models.notification import Notification, NotificationProfile
|
||||||
|
from judge.models.test_formatter import TestFormatterModel
|
||||||
|
|
||||||
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
||||||
revisions.register(Problem, follow=["language_limits"])
|
revisions.register(Problem, follow=["language_limits"])
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from judge.models.profile import Profile
|
from judge.models.profile import Profile
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
__all__ = ["BookMark"]
|
__all__ = ["BookMark"]
|
||||||
|
|
||||||
|
@ -21,12 +22,9 @@ class BookMark(models.Model):
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
linked_object = GenericForeignKey("content_type", "object_id")
|
linked_object = GenericForeignKey("content_type", "object_id")
|
||||||
|
|
||||||
def get_bookmark(self, user):
|
@cache_wrapper(prefix="BMgb")
|
||||||
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user)
|
def is_bookmarked_by(self, user):
|
||||||
if userqueryset.exists():
|
return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("bookmark")
|
verbose_name = _("bookmark")
|
||||||
|
@ -55,11 +53,22 @@ class MakeBookMark(models.Model):
|
||||||
verbose_name_plural = _("make bookmarks")
|
verbose_name_plural = _("make bookmarks")
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gocb", expected_type=BookMark)
|
||||||
|
def _get_or_create_bookmark(content_type, object_id):
|
||||||
|
bookmark, created = BookMark.objects.get_or_create(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=object_id,
|
||||||
|
)
|
||||||
|
return bookmark
|
||||||
|
|
||||||
|
|
||||||
class Bookmarkable:
|
class Bookmarkable:
|
||||||
def get_or_create_bookmark(self):
|
def get_or_create_bookmark(self):
|
||||||
if self.bookmark.count():
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
return self.bookmark.first()
|
object_id = self.pk
|
||||||
new_bookmark = BookMark()
|
return _get_or_create_bookmark(content_type, object_id)
|
||||||
new_bookmark.linked_object = self
|
|
||||||
new_bookmark.save()
|
|
||||||
return new_bookmark
|
def dirty_bookmark(bookmark, profile):
|
||||||
|
bookmark.is_bookmarked_by.dirty(bookmark, profile)
|
||||||
|
_get_or_create_bookmark.dirty(bookmark.content_type, bookmark.object_id)
|
||||||
|
|
|
@ -54,13 +54,3 @@ ACE_THEMES = (
|
||||||
("vibrant_ink", "Vibrant Ink"),
|
("vibrant_ink", "Vibrant Ink"),
|
||||||
("xcode", "XCode"),
|
("xcode", "XCode"),
|
||||||
)
|
)
|
||||||
|
|
||||||
MATH_ENGINES_CHOICES = (
|
|
||||||
("tex", _("Leave as LaTeX")),
|
|
||||||
("svg", _("SVG with PNG fallback")),
|
|
||||||
("mml", _("MathML only")),
|
|
||||||
("jax", _("MathJax with SVG/PNG fallback")),
|
|
||||||
("auto", _("Detect best quality")),
|
|
||||||
)
|
|
||||||
|
|
||||||
EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax")
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ from judge.models.interface import BlogPost
|
||||||
from judge.models.problem import Problem, Solution
|
from judge.models.problem import Problem, Solution
|
||||||
from judge.models.profile import Profile
|
from judge.models.profile import Profile
|
||||||
from judge.utils.cachedict import CacheDict
|
from judge.utils.cachedict import CacheDict
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
|
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
|
||||||
|
@ -56,6 +57,7 @@ class Comment(MPTTModel):
|
||||||
related_name="replies",
|
related_name="replies",
|
||||||
on_delete=CASCADE,
|
on_delete=CASCADE,
|
||||||
)
|
)
|
||||||
|
revision_count = models.PositiveIntegerField(default=1)
|
||||||
|
|
||||||
versions = VersionRelation()
|
versions = VersionRelation()
|
||||||
|
|
||||||
|
@ -71,19 +73,14 @@ class Comment(MPTTModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def most_recent(cls, user, n, batch=None, organization=None):
|
def most_recent(cls, user, n, batch=None, organization=None):
|
||||||
queryset = (
|
queryset = cls.objects.filter(hidden=False).order_by("-id")
|
||||||
cls.objects.filter(hidden=False)
|
|
||||||
.select_related("author__user")
|
|
||||||
.defer("author__about", "body")
|
|
||||||
.order_by("-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
if organization:
|
if organization:
|
||||||
queryset = queryset.filter(author__in=organization.members.all())
|
queryset = queryset.filter(author__in=organization.members.all())
|
||||||
|
|
||||||
problem_access = CacheDict(lambda p: p.is_accessible_by(user))
|
problem_access = CacheDict(lambda p: p.is_accessible_by(user))
|
||||||
contest_access = CacheDict(lambda c: c.is_accessible_by(user))
|
contest_access = CacheDict(lambda c: c.is_accessible_by(user))
|
||||||
blog_access = CacheDict(lambda b: b.can_see(user))
|
blog_access = CacheDict(lambda b: b.is_accessible_by(user))
|
||||||
|
|
||||||
if n == -1:
|
if n == -1:
|
||||||
n = len(queryset)
|
n = len(queryset)
|
||||||
|
@ -118,10 +115,6 @@ class Comment(MPTTModel):
|
||||||
query = Comment.filter(parent=self)
|
query = Comment.filter(parent=self)
|
||||||
return len(query)
|
return len(query)
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def get_revisions(self):
|
|
||||||
return self.versions.count()
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def page_title(self):
|
def page_title(self):
|
||||||
if isinstance(self.linked_object, Problem):
|
if isinstance(self.linked_object, Problem):
|
||||||
|
@ -177,3 +170,10 @@ class CommentLock(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.page)
|
return str(self.page)
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gcc")
|
||||||
|
def get_visible_comment_count(content_type, object_id):
|
||||||
|
return Comment.objects.filter(
|
||||||
|
content_type=content_type, object_id=object_id, hidden=False
|
||||||
|
).count()
|
||||||
|
|
|
@ -2,11 +2,14 @@ from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import CASCADE, Q
|
from django.db.models import CASCADE, Q
|
||||||
|
from django.db.models.signals import m2m_changed
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
from lupa import LuaRuntime
|
from lupa import LuaRuntime
|
||||||
from moss import (
|
from moss import (
|
||||||
|
@ -25,6 +28,7 @@ from judge.ratings import rate_contest
|
||||||
from judge.models.pagevote import PageVotable
|
from judge.models.pagevote import PageVotable
|
||||||
from judge.models.bookmark import Bookmarkable
|
from judge.models.bookmark import Bookmarkable
|
||||||
from judge.fulltext import SearchManager
|
from judge.fulltext import SearchManager
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Contest",
|
"Contest",
|
||||||
|
@ -35,6 +39,9 @@ __all__ = [
|
||||||
"Rating",
|
"Rating",
|
||||||
"ContestProblemClarification",
|
"ContestProblemClarification",
|
||||||
"ContestsSummary",
|
"ContestsSummary",
|
||||||
|
"OfficialContest",
|
||||||
|
"OfficialContestCategory",
|
||||||
|
"OfficialContestLocation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -310,6 +317,15 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
||||||
validators=[MinValueValidator(0), MaxValueValidator(10)],
|
validators=[MinValueValidator(0), MaxValueValidator(10)],
|
||||||
help_text=_("Number of digits to round points to."),
|
help_text=_("Number of digits to round points to."),
|
||||||
)
|
)
|
||||||
|
rate_limit = models.PositiveIntegerField(
|
||||||
|
verbose_name=(_("rate limit")),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
||||||
|
help_text=_(
|
||||||
|
"Maximum number of submissions per minute. Leave empty if you don't want rate limit."
|
||||||
|
),
|
||||||
|
)
|
||||||
comments = GenericRelation("Comment")
|
comments = GenericRelation("Comment")
|
||||||
pagevote = GenericRelation("PageVote")
|
pagevote = GenericRelation("PageVote")
|
||||||
bookmark = GenericRelation("BookMark")
|
bookmark = GenericRelation("BookMark")
|
||||||
|
@ -446,28 +462,44 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
||||||
def ended(self):
|
def ended(self):
|
||||||
return self.end_time < self._now
|
return self.end_time < self._now
|
||||||
|
|
||||||
@cached_property
|
@cache_wrapper(prefix="Coai")
|
||||||
def author_ids(self):
|
def _author_ids(self):
|
||||||
return Contest.authors.through.objects.filter(contest=self).values_list(
|
return set(
|
||||||
"profile_id", flat=True
|
Contest.authors.through.objects.filter(contest=self).values_list(
|
||||||
|
"profile_id", flat=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cache_wrapper(prefix="Coci")
|
||||||
def editor_ids(self):
|
def _curator_ids(self):
|
||||||
return self.author_ids.union(
|
return set(
|
||||||
Contest.curators.through.objects.filter(contest=self).values_list(
|
Contest.curators.through.objects.filter(contest=self).values_list(
|
||||||
"profile_id", flat=True
|
"profile_id", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cache_wrapper(prefix="Coti")
|
||||||
def tester_ids(self):
|
def _tester_ids(self):
|
||||||
return Contest.testers.through.objects.filter(contest=self).values_list(
|
return set(
|
||||||
"profile_id", flat=True
|
Contest.testers.through.objects.filter(contest=self).values_list(
|
||||||
|
"profile_id", flat=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def author_ids(self):
|
||||||
|
return self._author_ids()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def editor_ids(self):
|
||||||
|
return self.author_ids.union(self._curator_ids())
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def tester_ids(self):
|
||||||
|
return self._tester_ids()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return f"{self.name} ({self.key})"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("contest_view", args=(self.key,))
|
return reverse("contest_view", args=(self.key,))
|
||||||
|
@ -632,6 +664,20 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
||||||
verbose_name_plural = _("contests")
|
verbose_name_plural = _("contests")
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed, sender=Contest.organizations.through)
|
||||||
|
def update_organization_private(sender, instance, **kwargs):
|
||||||
|
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
|
||||||
|
instance.is_organization_private = instance.organizations.exists()
|
||||||
|
instance.save(update_fields=["is_organization_private"])
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed, sender=Contest.private_contestants.through)
|
||||||
|
def update_private(sender, instance, **kwargs):
|
||||||
|
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
|
||||||
|
instance.is_private = instance.private_contestants.exists()
|
||||||
|
instance.save(update_fields=["is_private"])
|
||||||
|
|
||||||
|
|
||||||
class ContestParticipation(models.Model):
|
class ContestParticipation(models.Model):
|
||||||
LIVE = 0
|
LIVE = 0
|
||||||
SPECTATE = -1
|
SPECTATE = -1
|
||||||
|
@ -920,6 +966,7 @@ class ContestsSummary(models.Model):
|
||||||
max_length=20,
|
max_length=20,
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
results = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("contests summary")
|
verbose_name = _("contests summary")
|
||||||
|
@ -930,3 +977,53 @@ class ContestsSummary(models.Model):
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("contests_summary", args=[self.key])
|
return reverse("contests_summary", args=[self.key])
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestCategory(models.Model):
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50, verbose_name=_("official contest category"), unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("official contest category")
|
||||||
|
verbose_name_plural = _("official contest categories")
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestLocation(models.Model):
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50, verbose_name=_("official contest location"), unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("official contest location")
|
||||||
|
verbose_name_plural = _("official contest locations")
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContest(models.Model):
|
||||||
|
contest = models.OneToOneField(
|
||||||
|
Contest,
|
||||||
|
verbose_name=_("contest"),
|
||||||
|
related_name="official",
|
||||||
|
on_delete=CASCADE,
|
||||||
|
)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
OfficialContestCategory,
|
||||||
|
verbose_name=_("contest category"),
|
||||||
|
on_delete=CASCADE,
|
||||||
|
)
|
||||||
|
year = models.PositiveIntegerField(verbose_name=_("year"))
|
||||||
|
location = models.ForeignKey(
|
||||||
|
OfficialContestLocation,
|
||||||
|
verbose_name=_("contest location"),
|
||||||
|
on_delete=CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("official contest")
|
||||||
|
verbose_name_plural = _("official contests")
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from judge.models import Contest
|
from judge.models import BlogPost, Problem
|
||||||
from judge.models.profile import Organization, Profile
|
from judge.models.profile import Organization, Profile
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Course",
|
|
||||||
"CourseRole",
|
|
||||||
"CourseResource",
|
|
||||||
"CourseAssignment",
|
|
||||||
]
|
|
||||||
|
|
||||||
course_directory_file = ""
|
class RoleInCourse(models.TextChoices):
|
||||||
|
STUDENT = "ST", _("Student")
|
||||||
|
ASSISTANT = "AS", _("Assistant")
|
||||||
|
TEACHER = "TE", _("Teacher")
|
||||||
|
|
||||||
|
|
||||||
|
EDITABLE_ROLES = (RoleInCourse.TEACHER, RoleInCourse.ASSISTANT)
|
||||||
|
|
||||||
|
|
||||||
class Course(models.Model):
|
class Course(models.Model):
|
||||||
|
@ -20,10 +22,7 @@ class Course(models.Model):
|
||||||
max_length=128,
|
max_length=128,
|
||||||
verbose_name=_("course name"),
|
verbose_name=_("course name"),
|
||||||
)
|
)
|
||||||
about = models.TextField(verbose_name=_("organization description"))
|
about = models.TextField(verbose_name=_("course description"))
|
||||||
ending_time = models.DateTimeField(
|
|
||||||
verbose_name=_("ending time"),
|
|
||||||
)
|
|
||||||
is_public = models.BooleanField(
|
is_public = models.BooleanField(
|
||||||
verbose_name=_("publicly visible"),
|
verbose_name=_("publicly visible"),
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -57,35 +56,50 @@ class Course(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
def get_absolute_url(self):
|
||||||
def is_editable_by(course, profile):
|
return reverse("course_detail", args=(self.slug,))
|
||||||
if profile.is_superuser:
|
|
||||||
return True
|
|
||||||
userquery = CourseRole.objects.filter(course=course, user=profile)
|
|
||||||
if userquery.exists():
|
|
||||||
if userquery[0].role == "AS" or userquery[0].role == "TE":
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_accessible_by(cls, course, profile):
|
def is_editable_by(cls, course, profile):
|
||||||
userqueryset = CourseRole.objects.filter(course=course, user=profile)
|
try:
|
||||||
if userqueryset.exists():
|
course_role = CourseRole.objects.get(course=course, user=profile)
|
||||||
return True
|
return course_role.role in EDITABLE_ROLES
|
||||||
else:
|
except CourseRole.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_students(cls, course):
|
def is_accessible_by(cls, course, profile):
|
||||||
return CourseRole.objects.filter(course=course, role="ST").values("user")
|
if not profile:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
course_role = CourseRole.objects.get(course=course, user=profile)
|
||||||
|
if course_role.course.is_public:
|
||||||
|
return True
|
||||||
|
return course_role.role in EDITABLE_ROLES
|
||||||
|
except CourseRole.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_assistants(cls, course):
|
def get_accessible_courses(cls, profile):
|
||||||
return CourseRole.objects.filter(course=course, role="AS").values("user")
|
return Course.objects.filter(
|
||||||
|
Q(is_public=True) | Q(courserole__role__in=EDITABLE_ROLES),
|
||||||
|
courserole__user=profile,
|
||||||
|
).distinct()
|
||||||
|
|
||||||
@classmethod
|
def _get_users_by_role(self, role):
|
||||||
def get_teachers(cls, course):
|
course_roles = CourseRole.objects.filter(course=self, role=role).select_related(
|
||||||
return CourseRole.objects.filter(course=course, role="TE").values("user")
|
"user"
|
||||||
|
)
|
||||||
|
return [course_role.user for course_role in course_roles]
|
||||||
|
|
||||||
|
def get_students(self):
|
||||||
|
return self._get_users_by_role(RoleInCourse.STUDENT)
|
||||||
|
|
||||||
|
def get_assistants(self):
|
||||||
|
return self._get_users_by_role(RoleInCourse.ASSISTANT)
|
||||||
|
|
||||||
|
def get_teachers(self):
|
||||||
|
return self._get_users_by_role(RoleInCourse.TEACHER)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_student(cls, course, profiles):
|
def add_student(cls, course, profiles):
|
||||||
|
@ -104,7 +118,7 @@ class Course(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class CourseRole(models.Model):
|
class CourseRole(models.Model):
|
||||||
course = models.OneToOneField(
|
course = models.ForeignKey(
|
||||||
Course,
|
Course,
|
||||||
verbose_name=_("course"),
|
verbose_name=_("course"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -114,14 +128,9 @@ class CourseRole(models.Model):
|
||||||
Profile,
|
Profile,
|
||||||
verbose_name=_("user"),
|
verbose_name=_("user"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name=_("user_of_course"),
|
related_name="course_roles",
|
||||||
)
|
)
|
||||||
|
|
||||||
class RoleInCourse(models.TextChoices):
|
|
||||||
STUDENT = "ST", _("Student")
|
|
||||||
ASSISTANT = "AS", _("Assistant")
|
|
||||||
TEACHER = "TE", _("Teacher")
|
|
||||||
|
|
||||||
role = models.CharField(
|
role = models.CharField(
|
||||||
max_length=2,
|
max_length=2,
|
||||||
choices=RoleInCourse.choices,
|
choices=RoleInCourse.choices,
|
||||||
|
@ -140,44 +149,19 @@ class CourseRole(models.Model):
|
||||||
couresrole.role = role
|
couresrole.role = role
|
||||||
couresrole.save()
|
couresrole.save()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("course", "user")
|
||||||
|
|
||||||
class CourseResource(models.Model):
|
|
||||||
course = models.OneToOneField(
|
class CourseLesson(models.Model):
|
||||||
|
course = models.ForeignKey(
|
||||||
Course,
|
Course,
|
||||||
verbose_name=_("course"),
|
verbose_name=_("course"),
|
||||||
on_delete=models.CASCADE,
|
related_name="lessons",
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
files = models.FileField(
|
|
||||||
verbose_name=_("course files"),
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
upload_to=course_directory_file,
|
|
||||||
)
|
|
||||||
description = models.CharField(
|
|
||||||
verbose_name=_("description"),
|
|
||||||
blank=True,
|
|
||||||
max_length=150,
|
|
||||||
)
|
|
||||||
order = models.IntegerField(null=True, default=None)
|
|
||||||
is_public = models.BooleanField(
|
|
||||||
verbose_name=_("publicly visible"),
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CourseAssignment(models.Model):
|
|
||||||
course = models.OneToOneField(
|
|
||||||
Course,
|
|
||||||
verbose_name=_("course"),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
contest = models.OneToOneField(
|
|
||||||
Contest,
|
|
||||||
verbose_name=_("contest"),
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
points = models.FloatField(
|
title = models.TextField(verbose_name=_("course title"))
|
||||||
verbose_name=_("points"),
|
content = models.TextField(verbose_name=_("course content"))
|
||||||
)
|
problems = models.ManyToManyField(Problem, verbose_name=_("problem"), blank=True)
|
||||||
|
order = models.IntegerField(verbose_name=_("order"), default=0)
|
||||||
|
points = models.IntegerField(verbose_name=_("points"))
|
||||||
|
|
|
@ -13,6 +13,7 @@ from mptt.models import MPTTModel
|
||||||
from judge.models.profile import Organization, Profile
|
from judge.models.profile import Organization, Profile
|
||||||
from judge.models.pagevote import PageVotable
|
from judge.models.pagevote import PageVotable
|
||||||
from judge.models.bookmark import Bookmarkable
|
from judge.models.bookmark import Bookmarkable
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
|
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("blog_post", args=(self.id, self.slug))
|
return reverse("blog_post", args=(self.id, self.slug))
|
||||||
|
|
||||||
def can_see(self, user):
|
def is_accessible_by(self, user):
|
||||||
if self.visible and self.publish_on <= timezone.now():
|
if self.visible and self.publish_on <= timezone.now():
|
||||||
if not self.is_organization_private:
|
if not self.is_organization_private:
|
||||||
return True
|
return True
|
||||||
|
@ -132,6 +133,10 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
|
||||||
and self.authors.filter(id=user.profile.id).exists()
|
and self.authors.filter(id=user.profile.id).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="BPga", expected_type=models.query.QuerySet)
|
||||||
|
def get_authors(self):
|
||||||
|
return self.authors.only("id")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (("edit_all_post", _("Edit all posts")),)
|
permissions = (("edit_all_post", _("Edit all posts")),)
|
||||||
verbose_name = _("blog post")
|
verbose_name = _("blog post")
|
||||||
|
|
|
@ -31,11 +31,8 @@ class PageVote(models.Model):
|
||||||
|
|
||||||
@cache_wrapper(prefix="PVvs")
|
@cache_wrapper(prefix="PVvs")
|
||||||
def vote_score(self, user):
|
def vote_score(self, user):
|
||||||
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
|
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user).first()
|
||||||
if page_vote.exists():
|
return page_vote.score if page_vote else 0
|
||||||
return page_vote.first().score
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"pagevote for {self.linked_object}"
|
return f"pagevote for {self.linked_object}"
|
||||||
|
@ -52,11 +49,22 @@ class PageVoteVoter(models.Model):
|
||||||
verbose_name_plural = _("pagevote votes")
|
verbose_name_plural = _("pagevote votes")
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gocp", expected_type=PageVote)
|
||||||
|
def _get_or_create_pagevote(content_type, object_id):
|
||||||
|
pagevote, created = PageVote.objects.get_or_create(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=object_id,
|
||||||
|
)
|
||||||
|
return pagevote
|
||||||
|
|
||||||
|
|
||||||
class PageVotable:
|
class PageVotable:
|
||||||
def get_or_create_pagevote(self):
|
def get_or_create_pagevote(self):
|
||||||
if self.pagevote.count():
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
return self.pagevote.first()
|
object_id = self.pk
|
||||||
new_pagevote = PageVote()
|
return _get_or_create_pagevote(content_type, object_id)
|
||||||
new_pagevote.linked_object = self
|
|
||||||
new_pagevote.save()
|
|
||||||
return new_pagevote
|
def dirty_pagevote(pagevote, profile):
|
||||||
|
pagevote.vote_score.dirty(pagevote, profile)
|
||||||
|
_get_or_create_pagevote.dirty(pagevote.content_type, pagevote.object_id)
|
||||||
|
|
|
@ -11,6 +11,8 @@ from django.db.models.functions import Coalesce
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.db.models.signals import m2m_changed
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from judge.fulltext import SearchQuerySet
|
from judge.fulltext import SearchQuerySet
|
||||||
from judge.models.pagevote import PageVotable
|
from judge.models.pagevote import PageVotable
|
||||||
|
@ -22,6 +24,7 @@ from judge.models.problem_data import (
|
||||||
problem_data_storage,
|
problem_data_storage,
|
||||||
problem_directory_file_helper,
|
problem_directory_file_helper,
|
||||||
)
|
)
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ProblemGroup",
|
"ProblemGroup",
|
||||||
|
@ -437,6 +440,10 @@ class Problem(models.Model, PageVotable, Bookmarkable):
|
||||||
"profile_id", flat=True
|
"profile_id", flat=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Pga", expected_type=models.query.QuerySet)
|
||||||
|
def get_authors(self):
|
||||||
|
return self.authors.only("id")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def editor_ids(self):
|
def editor_ids(self):
|
||||||
return self.author_ids.union(
|
return self.author_ids.union(
|
||||||
|
@ -554,21 +561,36 @@ class Problem(models.Model, PageVotable, Bookmarkable):
|
||||||
cache.set(key, result)
|
cache.set(key, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def handle_code_change(self):
|
||||||
|
has_data = hasattr(self, "data_files")
|
||||||
|
has_pdf = bool(self.pdf_description)
|
||||||
|
if not has_data and not has_pdf:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
problem_data_storage.rename(self.__original_code, self.code)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if has_pdf:
|
||||||
|
self.pdf_description.name = problem_directory_file_helper(
|
||||||
|
self.code, self.pdf_description.name
|
||||||
|
)
|
||||||
|
super().save(update_fields=["pdf_description"])
|
||||||
|
|
||||||
|
if has_data:
|
||||||
|
self.data_files._update_code(self.__original_code, self.code)
|
||||||
|
|
||||||
|
def save(self, should_move_data=True, *args, **kwargs):
|
||||||
|
code_changed = self.__original_code and self.code != self.__original_code
|
||||||
super(Problem, self).save(*args, **kwargs)
|
super(Problem, self).save(*args, **kwargs)
|
||||||
if self.__original_code and self.code != self.__original_code:
|
if code_changed and should_move_data:
|
||||||
if hasattr(self, "data_files") or self.pdf_description:
|
self.handle_code_change()
|
||||||
try:
|
|
||||||
problem_data_storage.rename(self.__original_code, self.code)
|
def delete(self, *args, **kwargs):
|
||||||
except OSError as e:
|
super().delete(*args, **kwargs)
|
||||||
if e.errno != errno.ENOENT:
|
problem_data_storage.delete_directory(self.code)
|
||||||
raise
|
|
||||||
if self.pdf_description:
|
|
||||||
self.pdf_description.name = problem_directory_file_helper(
|
|
||||||
self.code, self.pdf_description.name
|
|
||||||
)
|
|
||||||
if hasattr(self, "data_files"):
|
|
||||||
self.data_files._update_code(self.__original_code, self.code)
|
|
||||||
|
|
||||||
save.alters_data = True
|
save.alters_data = True
|
||||||
|
|
||||||
|
@ -682,6 +704,10 @@ class Solution(models.Model, PageVotable, Bookmarkable):
|
||||||
else:
|
else:
|
||||||
return reverse("problem_editorial", args=[problem.code])
|
return reverse("problem_editorial", args=[problem.code])
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Sga", expected_type=models.query.QuerySet)
|
||||||
|
def get_authors(self):
|
||||||
|
return self.authors.only("id")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Editorial for %s") % self.problem.name
|
return _("Editorial for %s") % self.problem.name
|
||||||
|
|
||||||
|
@ -719,3 +745,10 @@ class ProblemPointsVote(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.voter}: {self.points} for {self.problem.code}"
|
return f"{self.voter}: {self.points} for {self.problem.code}"
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed, sender=Problem.organizations.through)
|
||||||
|
def update_organization_private(sender, instance, **kwargs):
|
||||||
|
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
|
||||||
|
instance.is_organization_private = instance.organizations.exists()
|
||||||
|
instance.save(update_fields=["is_organization_private"])
|
||||||
|
|
|
@ -38,7 +38,7 @@ CHECKERS = (
|
||||||
("identical", _("Byte identical")),
|
("identical", _("Byte identical")),
|
||||||
("linecount", _("Line-by-line")),
|
("linecount", _("Line-by-line")),
|
||||||
("custom", _("Custom checker (PY)")),
|
("custom", _("Custom checker (PY)")),
|
||||||
("customval", _("Custom validator (CPP)")),
|
("customcpp", _("Custom checker (CPP)")),
|
||||||
("interact", _("Interactive")),
|
("interact", _("Interactive")),
|
||||||
("testlib", _("Testlib")),
|
("testlib", _("Testlib")),
|
||||||
)
|
)
|
||||||
|
@ -90,8 +90,8 @@ class ProblemData(models.Model):
|
||||||
upload_to=problem_directory_file,
|
upload_to=problem_directory_file,
|
||||||
validators=[FileExtensionValidator(allowed_extensions=["py"])],
|
validators=[FileExtensionValidator(allowed_extensions=["py"])],
|
||||||
)
|
)
|
||||||
custom_validator = models.FileField(
|
custom_checker_cpp = models.FileField(
|
||||||
verbose_name=_("custom validator file"),
|
verbose_name=_("custom cpp checker file"),
|
||||||
storage=problem_data_storage,
|
storage=problem_data_storage,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -186,9 +186,9 @@ class ProblemData(models.Model):
|
||||||
self.custom_checker.name = problem_directory_file_helper(
|
self.custom_checker.name = problem_directory_file_helper(
|
||||||
new, self.custom_checker.name
|
new, self.custom_checker.name
|
||||||
)
|
)
|
||||||
if self.custom_validator:
|
if self.custom_checker_cpp:
|
||||||
self.custom_validator.name = problem_directory_file_helper(
|
self.custom_checker_cpp.name = problem_directory_file_helper(
|
||||||
new, self.custom_validator.name
|
new, self.custom_checker_cpp.name
|
||||||
)
|
)
|
||||||
if self.interactive_judge:
|
if self.interactive_judge:
|
||||||
self.interactive_judge.name = problem_directory_file_helper(
|
self.interactive_judge.name = problem_directory_file_helper(
|
||||||
|
|
|
@ -17,7 +17,7 @@ from django.db.models.signals import post_save, pre_save
|
||||||
from fernet_fields import EncryptedCharField
|
from fernet_fields import EncryptedCharField
|
||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
|
|
||||||
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
|
from judge.models.choices import ACE_THEMES, TIMEZONE
|
||||||
from judge.models.runtime import Language
|
from judge.models.runtime import Language
|
||||||
from judge.ratings import rating_class
|
from judge.ratings import rating_class
|
||||||
from judge.caching import cache_wrapper
|
from judge.caching import cache_wrapper
|
||||||
|
@ -26,6 +26,15 @@ from judge.caching import cache_wrapper
|
||||||
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
|
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
|
||||||
|
|
||||||
|
|
||||||
|
TSHIRT_SIZES = (
|
||||||
|
("S", "Small (S)"),
|
||||||
|
("M", "Medium (M)"),
|
||||||
|
("L", "Large (L)"),
|
||||||
|
("XL", "Extra Large (XL)"),
|
||||||
|
("XXL", "2 Extra Large (XXL)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EncryptedNullCharField(EncryptedCharField):
|
class EncryptedNullCharField(EncryptedCharField):
|
||||||
def get_prep_value(self, value):
|
def get_prep_value(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
|
@ -55,7 +64,9 @@ class Organization(models.Model):
|
||||||
verbose_name=_("short name"),
|
verbose_name=_("short name"),
|
||||||
help_text=_("Displayed beside user name during contests"),
|
help_text=_("Displayed beside user name during contests"),
|
||||||
)
|
)
|
||||||
about = models.TextField(verbose_name=_("organization description"))
|
about = models.CharField(
|
||||||
|
max_length=10000, verbose_name=_("organization description")
|
||||||
|
)
|
||||||
registrant = models.ForeignKey(
|
registrant = models.ForeignKey(
|
||||||
"Profile",
|
"Profile",
|
||||||
verbose_name=_("registrant"),
|
verbose_name=_("registrant"),
|
||||||
|
@ -139,6 +150,14 @@ class Organization(models.Model):
|
||||||
def get_submissions_url(self):
|
def get_submissions_url(self):
|
||||||
return reverse("organization_submissions", args=(self.id, self.slug))
|
return reverse("organization_submissions", args=(self.id, self.slug))
|
||||||
|
|
||||||
|
@cache_wrapper("Oia")
|
||||||
|
def is_admin(self, profile):
|
||||||
|
return self.admins.filter(id=profile.id).exists()
|
||||||
|
|
||||||
|
@cache_wrapper("Oim")
|
||||||
|
def is_member(self, profile):
|
||||||
|
return profile in self
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -154,7 +173,9 @@ class Profile(models.Model):
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
User, verbose_name=_("user associated"), on_delete=models.CASCADE
|
User, verbose_name=_("user associated"), on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
about = models.TextField(verbose_name=_("self-description"), null=True, blank=True)
|
about = models.CharField(
|
||||||
|
max_length=10000, verbose_name=_("self-description"), null=True, blank=True
|
||||||
|
)
|
||||||
timezone = models.CharField(
|
timezone = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name=_("location"),
|
verbose_name=_("location"),
|
||||||
|
@ -201,19 +222,7 @@ class Profile(models.Model):
|
||||||
help_text=_("User will not be ranked."),
|
help_text=_("User will not be ranked."),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
is_banned_problem_voting = models.BooleanField(
|
|
||||||
verbose_name=_("banned from voting"),
|
|
||||||
help_text=_("User will not be able to vote on problems' point values."),
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
rating = models.IntegerField(null=True, default=None, db_index=True)
|
rating = models.IntegerField(null=True, default=None, db_index=True)
|
||||||
user_script = models.TextField(
|
|
||||||
verbose_name=_("user script"),
|
|
||||||
default="",
|
|
||||||
blank=True,
|
|
||||||
max_length=65536,
|
|
||||||
help_text=_("User-defined JavaScript for site customization."),
|
|
||||||
)
|
|
||||||
current_contest = models.OneToOneField(
|
current_contest = models.OneToOneField(
|
||||||
"ContestParticipation",
|
"ContestParticipation",
|
||||||
verbose_name=_("current contest"),
|
verbose_name=_("current contest"),
|
||||||
|
@ -222,13 +231,6 @@ class Profile(models.Model):
|
||||||
related_name="+",
|
related_name="+",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
math_engine = models.CharField(
|
|
||||||
verbose_name=_("math engine"),
|
|
||||||
choices=MATH_ENGINES_CHOICES,
|
|
||||||
max_length=4,
|
|
||||||
default=settings.MATHOID_DEFAULT_TYPE,
|
|
||||||
help_text=_("the rendering engine used to render math"),
|
|
||||||
)
|
|
||||||
is_totp_enabled = models.BooleanField(
|
is_totp_enabled = models.BooleanField(
|
||||||
verbose_name=_("2FA enabled"),
|
verbose_name=_("2FA enabled"),
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -260,23 +262,9 @@ class Profile(models.Model):
|
||||||
max_length=300,
|
max_length=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
@cache_wrapper(prefix="Pgbi2")
|
|
||||||
def _get_basic_info(self):
|
|
||||||
profile_image_url = None
|
|
||||||
if self.profile_image:
|
|
||||||
profile_image_url = self.profile_image.url
|
|
||||||
return {
|
|
||||||
"first_name": self.user.first_name,
|
|
||||||
"last_name": self.user.last_name,
|
|
||||||
"email": self.user.email,
|
|
||||||
"username": self.user.username,
|
|
||||||
"mute": self.mute,
|
|
||||||
"profile_image_url": profile_image_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _cached_info(self):
|
def _cached_info(self):
|
||||||
return self._get_basic_info()
|
return _get_basic_info(self.id)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def organization(self):
|
def organization(self):
|
||||||
|
@ -290,11 +278,11 @@ class Profile(models.Model):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def first_name(self):
|
def first_name(self):
|
||||||
return self._cached_info["first_name"]
|
return self._cached_info.get("first_name", "")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def last_name(self):
|
def last_name(self):
|
||||||
return self._cached_info["last_name"]
|
return self._cached_info.get("last_name", "")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def email(self):
|
def email(self):
|
||||||
|
@ -304,9 +292,17 @@ class Profile(models.Model):
|
||||||
def is_muted(self):
|
def is_muted(self):
|
||||||
return self._cached_info["mute"]
|
return self._cached_info["mute"]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def cached_display_rank(self):
|
||||||
|
return self._cached_info.get("display_rank")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def cached_rating(self):
|
||||||
|
return self._cached_info.get("rating")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def profile_image_url(self):
|
def profile_image_url(self):
|
||||||
return self._cached_info["profile_image_url"]
|
return self._cached_info.get("profile_image_url")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def count_unseen_notifications(self):
|
def count_unseen_notifications(self):
|
||||||
|
@ -398,7 +394,7 @@ class Profile(models.Model):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def css_class(self):
|
def css_class(self):
|
||||||
return self.get_user_css_class(self.display_rank, self.rating)
|
return self.get_user_css_class(self.cached_display_rank, self.cached_rating)
|
||||||
|
|
||||||
def get_friends(self): # list of ids, including you
|
def get_friends(self): # list of ids, including you
|
||||||
friend_obj = self.following_users.prefetch_related("users")
|
friend_obj = self.following_users.prefetch_related("users")
|
||||||
|
@ -412,13 +408,16 @@ class Profile(models.Model):
|
||||||
if not self.user.is_authenticated:
|
if not self.user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
profile_id = self.id
|
profile_id = self.id
|
||||||
return (
|
return org.is_admin(self) or self.user.is_superuser
|
||||||
org.admins.filter(id=profile_id).exists()
|
|
||||||
or org.registrant_id == profile_id
|
@classmethod
|
||||||
or self.user.is_superuser
|
def prefetch_profile_cache(self, profile_ids):
|
||||||
)
|
_get_basic_info.prefetch_multi([(pid,) for pid in profile_ids])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["is_unlisted", "performance_points"]),
|
||||||
|
]
|
||||||
permissions = (
|
permissions = (
|
||||||
("test_site", "Shows in-progress development stuff"),
|
("test_site", "Shows in-progress development stuff"),
|
||||||
("totp", "Edit TOTP settings"),
|
("totp", "Edit TOTP settings"),
|
||||||
|
@ -427,6 +426,36 @@ class Profile(models.Model):
|
||||||
verbose_name_plural = _("user profiles")
|
verbose_name_plural = _("user profiles")
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileInfo(models.Model):
|
||||||
|
profile = models.OneToOneField(
|
||||||
|
Profile,
|
||||||
|
verbose_name=_("profile associated"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="info",
|
||||||
|
)
|
||||||
|
tshirt_size = models.CharField(
|
||||||
|
max_length=5,
|
||||||
|
choices=TSHIRT_SIZES,
|
||||||
|
verbose_name=_("t-shirt size"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
date_of_birth = models.DateField(
|
||||||
|
verbose_name=_("date of birth"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
address = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("address"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.profile.user.username}'s Info"
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRequest(models.Model):
|
class OrganizationRequest(models.Model):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
Profile,
|
Profile,
|
||||||
|
@ -468,11 +497,7 @@ class Friend(models.Model):
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_friend(self, current_user, new_friend):
|
def is_friend(self, current_user, new_friend):
|
||||||
try:
|
try:
|
||||||
return (
|
return current_user.following_users.filter(users=new_friend).exists()
|
||||||
current_user.following_users.get()
|
|
||||||
.users.filter(user=new_friend.user)
|
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -506,7 +531,7 @@ class Friend(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class OrganizationProfile(models.Model):
|
class OrganizationProfile(models.Model):
|
||||||
users = models.ForeignKey(
|
profile = models.ForeignKey(
|
||||||
Profile,
|
Profile,
|
||||||
verbose_name=_("user"),
|
verbose_name=_("user"),
|
||||||
related_name="last_visit",
|
related_name="last_visit",
|
||||||
|
@ -525,37 +550,66 @@ class OrganizationProfile(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove_organization(self, users, organization):
|
def remove_organization(self, profile, organization):
|
||||||
organizationprofile = self.objects.filter(
|
organization_profile = self.objects.filter(
|
||||||
users=users, organization=organization
|
profile=profile, organization=organization
|
||||||
)
|
)
|
||||||
if organizationprofile.exists():
|
if organization_profile.exists():
|
||||||
organizationprofile.delete()
|
organization_profile.delete()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_organization(self, users, organization):
|
def add_organization(self, profile, organization):
|
||||||
self.remove_organization(users, organization)
|
self.remove_organization(profile, organization)
|
||||||
new_organization = OrganizationProfile(users=users, organization=organization)
|
new_row = OrganizationProfile(profile=profile, organization=organization)
|
||||||
new_organization.save()
|
new_row.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_most_recent_organizations(self, users):
|
def get_most_recent_organizations(cls, profile):
|
||||||
return self.objects.filter(users=users).order_by("-last_visit")[:5]
|
queryset = cls.objects.filter(profile=profile).order_by("-last_visit")[:5]
|
||||||
|
queryset = queryset.select_related("organization").defer("organization__about")
|
||||||
|
organizations = [op.organization for op in queryset]
|
||||||
|
|
||||||
|
return organizations
|
||||||
|
|
||||||
|
|
||||||
@receiver([post_save], sender=User)
|
@receiver([post_save], sender=User)
|
||||||
def on_user_save(sender, instance, **kwargs):
|
def on_user_save(sender, instance, **kwargs):
|
||||||
try:
|
try:
|
||||||
profile = instance.profile
|
profile = instance.profile
|
||||||
profile._get_basic_info.dirty(profile)
|
_get_basic_info.dirty(profile.id)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@receiver([pre_save], sender=Profile)
|
@cache_wrapper(prefix="Pgbi3", expected_type=dict)
|
||||||
def on_profile_save(sender, instance, **kwargs):
|
def _get_basic_info(profile_id):
|
||||||
if instance.id is None:
|
profile = (
|
||||||
return
|
Profile.objects.select_related("user")
|
||||||
prev = sender.objects.get(id=instance.id)
|
.only(
|
||||||
if prev.mute != instance.mute or prev.profile_image != instance.profile_image:
|
"id",
|
||||||
instance._get_basic_info.dirty(instance)
|
"mute",
|
||||||
|
"profile_image",
|
||||||
|
"user__username",
|
||||||
|
"user__email",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"display_rank",
|
||||||
|
"rating",
|
||||||
|
)
|
||||||
|
.get(id=profile_id)
|
||||||
|
)
|
||||||
|
user = profile.user
|
||||||
|
res = {
|
||||||
|
"email": user.email,
|
||||||
|
"username": user.username,
|
||||||
|
"mute": profile.mute,
|
||||||
|
"first_name": user.first_name or None,
|
||||||
|
"last_name": user.last_name or None,
|
||||||
|
"profile_image_url": profile.profile_image.url
|
||||||
|
if profile.profile_image
|
||||||
|
else None,
|
||||||
|
"display_rank": profile.display_rank,
|
||||||
|
"rating": profile.rating,
|
||||||
|
}
|
||||||
|
res = {k: v for k, v in res.items() if v is not None}
|
||||||
|
return res
|
||||||
|
|
|
@ -11,6 +11,7 @@ from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from judge.judgeapi import disconnect_judge
|
from judge.judgeapi import disconnect_judge
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
__all__ = ["Language", "RuntimeVersion", "Judge"]
|
__all__ = ["Language", "RuntimeVersion", "Judge"]
|
||||||
|
|
||||||
|
@ -147,14 +148,11 @@ class Language(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_language(cls):
|
def get_default_language(cls):
|
||||||
try:
|
return _get_default_language()
|
||||||
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
|
|
||||||
except Language.DoesNotExist:
|
|
||||||
return cls.get_python3()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_language_pk(cls):
|
def get_default_language_pk(cls):
|
||||||
return cls.get_default_language().pk
|
return _get_default_language().pk
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["key"]
|
ordering = ["key"]
|
||||||
|
@ -162,6 +160,14 @@ class Language(models.Model):
|
||||||
verbose_name_plural = _("languages")
|
verbose_name_plural = _("languages")
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gdl")
|
||||||
|
def _get_default_language():
|
||||||
|
try:
|
||||||
|
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
|
||||||
|
except Language.DoesNotExist:
|
||||||
|
return cls.get_python3()
|
||||||
|
|
||||||
|
|
||||||
class RuntimeVersion(models.Model):
|
class RuntimeVersion(models.Model):
|
||||||
language = models.ForeignKey(
|
language = models.ForeignKey(
|
||||||
Language,
|
Language,
|
||||||
|
|
|
@ -220,13 +220,7 @@ class Submission(models.Model):
|
||||||
def id_secret(self):
|
def id_secret(self):
|
||||||
return self.get_id_secret(self.id)
|
return self.get_id_secret(self.id)
|
||||||
|
|
||||||
def is_accessible_by(self, profile):
|
def is_accessible_by(self, profile, check_contest=True):
|
||||||
from judge.utils.problems import (
|
|
||||||
user_completed_ids,
|
|
||||||
user_tester_ids,
|
|
||||||
user_editable_ids,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not profile:
|
if not profile:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -236,15 +230,6 @@ class Submission(models.Model):
|
||||||
if profile.id == self.user_id:
|
if profile.id == self.user_id:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if problem_id in user_editable_ids(profile):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.problem_id in user_completed_ids(profile):
|
|
||||||
if self.problem.is_public:
|
|
||||||
return True
|
|
||||||
if problem_id in user_tester_ids(profile):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if user.has_perm("judge.change_submission"):
|
if user.has_perm("judge.change_submission"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -254,10 +239,26 @@ class Submission(models.Model):
|
||||||
if self.problem.is_public and user.has_perm("judge.view_public_submission"):
|
if self.problem.is_public and user.has_perm("judge.view_public_submission"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
contest = self.contest_object
|
if check_contest:
|
||||||
if contest and contest.is_editable_by(user):
|
contest = self.contest_object
|
||||||
|
if contest and contest.is_editable_by(user):
|
||||||
|
return True
|
||||||
|
|
||||||
|
from judge.utils.problems import (
|
||||||
|
user_completed_ids,
|
||||||
|
user_tester_ids,
|
||||||
|
user_editable_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
if problem_id in user_editable_ids(profile):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if self.problem_id in user_completed_ids(profile):
|
||||||
|
if self.problem.is_public:
|
||||||
|
return True
|
||||||
|
if problem_id in user_tester_ids(profile):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -276,6 +277,7 @@ class Submission(models.Model):
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["problem", "user", "-points"]),
|
models.Index(fields=["problem", "user", "-points"]),
|
||||||
models.Index(fields=["contest_object", "problem", "user", "-points"]),
|
models.Index(fields=["contest_object", "problem", "user", "-points"]),
|
||||||
|
models.Index(fields=["language", "result"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
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.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
BETA2 = 328.33**2
|
BETA2 = 328.33**2
|
||||||
RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
|
RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
|
||||||
MEAN_INIT = 1400.0
|
MEAN_INIT = 1400.0
|
||||||
|
@ -146,6 +145,8 @@ def recalculate_ratings(ranking, old_mean, times_ranked, historical_p):
|
||||||
|
|
||||||
def rate_contest(contest):
|
def rate_contest(contest):
|
||||||
from judge.models import Rating, Profile
|
from judge.models import Rating, Profile
|
||||||
|
from judge.models.profile import _get_basic_info
|
||||||
|
from judge.utils.users import get_contest_ratings, get_rating_rank
|
||||||
|
|
||||||
rating_subquery = Rating.objects.filter(user=OuterRef("user"))
|
rating_subquery = Rating.objects.filter(user=OuterRef("user"))
|
||||||
rating_sorted = rating_subquery.order_by("-contest__end_time")
|
rating_sorted = rating_subquery.order_by("-contest__end_time")
|
||||||
|
@ -237,6 +238,10 @@ def rate_contest(contest):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_get_basic_info.dirty_multi([(uid,) for uid in user_ids])
|
||||||
|
get_contest_ratings.dirty_multi([(uid,) for uid in user_ids])
|
||||||
|
get_rating_rank.dirty_multi([(uid,) for uid in user_ids])
|
||||||
|
|
||||||
|
|
||||||
RATING_LEVELS = [
|
RATING_LEVELS = [
|
||||||
"Newbie",
|
"Newbie",
|
||||||
|
|
|
@ -8,13 +8,13 @@ from django.core.cache.utils import make_template_fragment_key
|
||||||
from django.db.models.signals import post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
import judge
|
||||||
from judge.utils.problems import finished_submission
|
from judge.utils.problems import finished_submission
|
||||||
from .models import (
|
from .models import (
|
||||||
BlogPost,
|
BlogPost,
|
||||||
Comment,
|
Comment,
|
||||||
Contest,
|
Contest,
|
||||||
ContestSubmission,
|
ContestSubmission,
|
||||||
EFFECTIVE_MATH_ENGINES,
|
|
||||||
Judge,
|
Judge,
|
||||||
Language,
|
Language,
|
||||||
License,
|
License,
|
||||||
|
@ -23,6 +23,8 @@ from .models import (
|
||||||
Problem,
|
Problem,
|
||||||
Profile,
|
Profile,
|
||||||
Submission,
|
Submission,
|
||||||
|
NavigationBar,
|
||||||
|
Solution,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,21 +48,13 @@ def problem_update(sender, instance, **kwargs):
|
||||||
cache.delete_many(
|
cache.delete_many(
|
||||||
[
|
[
|
||||||
make_template_fragment_key("submission_problem", (instance.id,)),
|
make_template_fragment_key("submission_problem", (instance.id,)),
|
||||||
make_template_fragment_key("problem_feed", (instance.id,)),
|
|
||||||
"problem_tls:%s" % instance.id,
|
"problem_tls:%s" % instance.id,
|
||||||
"problem_mls:%s" % instance.id,
|
"problem_mls:%s" % instance.id,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
cache.delete_many(
|
cache.delete_many(
|
||||||
[
|
[
|
||||||
make_template_fragment_key("problem_html", (instance.id, engine, lang))
|
make_template_fragment_key("problem_html", (instance.id, lang))
|
||||||
for lang, _ in settings.LANGUAGES
|
|
||||||
for engine in EFFECTIVE_MATH_ENGINES
|
|
||||||
]
|
|
||||||
)
|
|
||||||
cache.delete_many(
|
|
||||||
[
|
|
||||||
make_template_fragment_key("problem_authors", (instance.id, lang))
|
|
||||||
for lang, _ in settings.LANGUAGES
|
for lang, _ in settings.LANGUAGES
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -70,6 +64,7 @@ def problem_update(sender, instance, **kwargs):
|
||||||
for lang, _ in settings.LANGUAGES
|
for lang, _ in settings.LANGUAGES
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
Problem.get_authors.dirty(instance)
|
||||||
|
|
||||||
for lang, _ in settings.LANGUAGES:
|
for lang, _ in settings.LANGUAGES:
|
||||||
unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang)))
|
unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang)))
|
||||||
|
@ -77,20 +72,21 @@ def problem_update(sender, instance, **kwargs):
|
||||||
|
|
||||||
@receiver(post_save, sender=Profile)
|
@receiver(post_save, sender=Profile)
|
||||||
def profile_update(sender, instance, **kwargs):
|
def profile_update(sender, instance, **kwargs):
|
||||||
|
judge.utils.users.get_points_rank.dirty(instance.id)
|
||||||
|
judge.utils.users.get_rating_rank.dirty(instance.id)
|
||||||
if hasattr(instance, "_updating_stats_only"):
|
if hasattr(instance, "_updating_stats_only"):
|
||||||
return
|
return
|
||||||
|
|
||||||
cache.delete_many(
|
cache.delete_many(
|
||||||
[
|
[make_template_fragment_key("user_about", (instance.id,))]
|
||||||
make_template_fragment_key("user_about", (instance.id, engine))
|
|
||||||
for engine in EFFECTIVE_MATH_ENGINES
|
|
||||||
]
|
|
||||||
+ [
|
+ [
|
||||||
make_template_fragment_key("org_member_count", (org_id,))
|
make_template_fragment_key("org_member_count", (org_id,))
|
||||||
for org_id in instance.organizations.values_list("id", flat=True)
|
for org_id in instance.organizations.values_list("id", flat=True)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
judge.models.profile._get_basic_info.dirty(instance.id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Contest)
|
@receiver(post_save, sender=Contest)
|
||||||
def contest_update(sender, instance, **kwargs):
|
def contest_update(sender, instance, **kwargs):
|
||||||
|
@ -99,10 +95,7 @@ def contest_update(sender, instance, **kwargs):
|
||||||
|
|
||||||
cache.delete_many(
|
cache.delete_many(
|
||||||
["generated-meta-contest:%d" % instance.id]
|
["generated-meta-contest:%d" % instance.id]
|
||||||
+ [
|
+ [make_template_fragment_key("contest_html", (instance.id,))]
|
||||||
make_template_fragment_key("contest_html", (instance.id, engine))
|
|
||||||
for engine in EFFECTIVE_MATH_ENGINES
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,19 +123,8 @@ def comment_update(sender, instance, **kwargs):
|
||||||
|
|
||||||
@receiver(post_save, sender=BlogPost)
|
@receiver(post_save, sender=BlogPost)
|
||||||
def post_update(sender, instance, **kwargs):
|
def post_update(sender, instance, **kwargs):
|
||||||
cache.delete_many(
|
cache.delete(make_template_fragment_key("post_content", (instance.id,)))
|
||||||
[
|
BlogPost.get_authors.dirty(instance)
|
||||||
make_template_fragment_key("post_summary", (instance.id,)),
|
|
||||||
"blog_slug:%d" % instance.id,
|
|
||||||
"blog_feed:%d" % instance.id,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
cache.delete_many(
|
|
||||||
[
|
|
||||||
make_template_fragment_key("post_content", (instance.id, engine))
|
|
||||||
for engine in EFFECTIVE_MATH_ENGINES
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Submission)
|
@receiver(post_delete, sender=Submission)
|
||||||
|
@ -159,12 +141,9 @@ def contest_submission_delete(sender, instance, **kwargs):
|
||||||
|
|
||||||
@receiver(post_save, sender=Organization)
|
@receiver(post_save, sender=Organization)
|
||||||
def organization_update(sender, instance, **kwargs):
|
def organization_update(sender, instance, **kwargs):
|
||||||
cache.delete_many(
|
cache.delete_many([make_template_fragment_key("organization_html", (instance.id,))])
|
||||||
[
|
for admin in instance.admins.all():
|
||||||
make_template_fragment_key("organization_html", (instance.id, engine))
|
Organization.is_admin.dirty(instance, admin)
|
||||||
for engine in EFFECTIVE_MATH_ENGINES
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_misc_config_i18n = [code for code, _ in settings.LANGUAGES]
|
_misc_config_i18n = [code for code, _ in settings.LANGUAGES]
|
||||||
|
@ -187,3 +166,13 @@ def contest_submission_update(sender, instance, **kwargs):
|
||||||
Submission.objects.filter(id=instance.submission_id).update(
|
Submission.objects.filter(id=instance.submission_id).update(
|
||||||
contest_object_id=instance.participation.contest_id
|
contest_object_id=instance.participation.contest_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=NavigationBar)
|
||||||
|
def navbar_update(sender, instance, **kwargs):
|
||||||
|
judge.template_context._nav_bar.dirty()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Solution)
|
||||||
|
def solution_update(sender, instance, **kwargs):
|
||||||
|
cache.delete(make_template_fragment_key("solution_content", (instance.id,)))
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.db import transaction
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
from reversion import revisions
|
from reversion import revisions
|
||||||
from social_core.backends.github import GithubOAuth2
|
from social_core.backends.github import GithubOAuth2
|
||||||
|
@ -65,13 +66,13 @@ class UsernameForm(forms.Form):
|
||||||
max_length=30,
|
max_length=30,
|
||||||
label="Username",
|
label="Username",
|
||||||
error_messages={
|
error_messages={
|
||||||
"invalid": "A username must contain letters, numbers, or underscores"
|
"invalid": _("A username must contain letters, numbers, or underscores")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_username(self):
|
def clean_username(self):
|
||||||
if User.objects.filter(username=self.cleaned_data["username"]).exists():
|
if User.objects.filter(username=self.cleaned_data["username"]).exists():
|
||||||
raise forms.ValidationError("Sorry, the username is taken.")
|
raise forms.ValidationError(_("Sorry, the username is taken."))
|
||||||
return self.cleaned_data["username"]
|
return self.cleaned_data["username"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ def choose_username(backend, user, username=None, *args, **kwargs):
|
||||||
request,
|
request,
|
||||||
"registration/username_select.html",
|
"registration/username_select.html",
|
||||||
{
|
{
|
||||||
"title": "Choose a username",
|
"title": _("Choose a username"),
|
||||||
"form": form,
|
"form": form,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -118,7 +119,7 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs):
|
||||||
backend.strategy.request,
|
backend.strategy.request,
|
||||||
"registration/profile_creation.html",
|
"registration/profile_creation.html",
|
||||||
{
|
{
|
||||||
"title": "Create your profile",
|
"title": _("Create your profile"),
|
||||||
"form": form,
|
"form": form,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from judge.utils.celery import Progress
|
||||||
__all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem")
|
__all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem")
|
||||||
|
|
||||||
|
|
||||||
def apply_submission_filter(queryset, id_range, languages, results, contest):
|
def apply_submission_filter(queryset, id_range, languages, results, contests):
|
||||||
if id_range:
|
if id_range:
|
||||||
start, end = id_range
|
start, end = id_range
|
||||||
queryset = queryset.filter(id__gte=start, id__lte=end)
|
queryset = queryset.filter(id__gte=start, id__lte=end)
|
||||||
|
@ -16,8 +16,8 @@ def apply_submission_filter(queryset, id_range, languages, results, contest):
|
||||||
queryset = queryset.filter(language_id__in=languages)
|
queryset = queryset.filter(language_id__in=languages)
|
||||||
if results:
|
if results:
|
||||||
queryset = queryset.filter(result__in=results)
|
queryset = queryset.filter(result__in=results)
|
||||||
if contest:
|
if contests:
|
||||||
queryset = queryset.filter(contest_object=contest)
|
queryset = queryset.filter(contest_object__in=contests)
|
||||||
queryset = queryset.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS)
|
queryset = queryset.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -6,7 +7,10 @@ from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils.functional import SimpleLazyObject, new_method_proxy
|
from django.utils.functional import SimpleLazyObject, new_method_proxy
|
||||||
|
|
||||||
|
from mptt.querysets import TreeQuerySet
|
||||||
|
|
||||||
from .models import MiscConfig, NavigationBar, Profile
|
from .models import MiscConfig, NavigationBar, Profile
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
class FixedSimpleLazyObject(SimpleLazyObject):
|
class FixedSimpleLazyObject(SimpleLazyObject):
|
||||||
|
@ -24,7 +28,6 @@ def get_resource(request):
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"PYGMENT_THEME": settings.PYGMENT_THEME,
|
|
||||||
"INLINE_JQUERY": settings.INLINE_JQUERY,
|
"INLINE_JQUERY": settings.INLINE_JQUERY,
|
||||||
"INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME,
|
"INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME,
|
||||||
"JQUERY_JS": settings.JQUERY_JS,
|
"JQUERY_JS": settings.JQUERY_JS,
|
||||||
|
@ -51,22 +54,28 @@ def comet_location(request):
|
||||||
return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll}
|
return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll}
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="nb", expected_type=TreeQuerySet)
|
||||||
|
def _nav_bar():
|
||||||
|
return NavigationBar.objects.all()
|
||||||
|
|
||||||
|
|
||||||
def __nav_tab(path):
|
def __nav_tab(path):
|
||||||
result = list(
|
nav_bar_list = list(_nav_bar())
|
||||||
NavigationBar.objects.extra(where=["%s REGEXP BINARY regex"], params=[path])[:1]
|
nav_bar_dict = {nb.id: nb for nb in nav_bar_list}
|
||||||
)
|
result = next((nb for nb in nav_bar_list if re.match(nb.regex, path)), None)
|
||||||
return (
|
if result:
|
||||||
result[0].get_ancestors(include_self=True).values_list("key", flat=True)
|
while result.parent_id:
|
||||||
if result
|
result = nav_bar_dict.get(result.parent_id)
|
||||||
else []
|
return result.key
|
||||||
)
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def general_info(request):
|
def general_info(request):
|
||||||
path = request.get_full_path()
|
path = request.get_full_path()
|
||||||
return {
|
return {
|
||||||
"nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)),
|
"nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)),
|
||||||
"nav_bar": NavigationBar.objects.all(),
|
"nav_bar": _nav_bar(),
|
||||||
"LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path,
|
"LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path,
|
||||||
"perms": PermWrapper(request.user),
|
"perms": PermWrapper(request.user),
|
||||||
}
|
}
|
||||||
|
@ -119,13 +128,3 @@ def site_name(request):
|
||||||
"SITE_LONG_NAME": settings.SITE_LONG_NAME,
|
"SITE_LONG_NAME": settings.SITE_LONG_NAME,
|
||||||
"SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL,
|
"SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def math_setting(request):
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
engine = request.profile.math_engine
|
|
||||||
else:
|
|
||||||
engine = settings.MATHOID_DEFAULT_TYPE
|
|
||||||
if engine == "auto":
|
|
||||||
engine = "jax"
|
|
||||||
return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax"}
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
from judge.models import Profile
|
from judge.models import Profile
|
||||||
|
|
||||||
|
@ -15,11 +16,13 @@ class LogUserAccessMiddleware(object):
|
||||||
hasattr(request, "user")
|
hasattr(request, "user")
|
||||||
and request.user.is_authenticated
|
and request.user.is_authenticated
|
||||||
and not getattr(request, "no_profile_update", False)
|
and not getattr(request, "no_profile_update", False)
|
||||||
|
and not cache.get(f"user_log_update_{request.user.id}")
|
||||||
):
|
):
|
||||||
updates = {"last_access": now()}
|
updates = {"last_access": now()}
|
||||||
# Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it.
|
# Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it.
|
||||||
if request.META.get(settings.META_REMOTE_ADDRESS_KEY):
|
if request.META.get(settings.META_REMOTE_ADDRESS_KEY):
|
||||||
updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY)
|
updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY)
|
||||||
Profile.objects.filter(user_id=request.user.pk).update(**updates)
|
Profile.objects.filter(user_id=request.user.pk).update(**updates)
|
||||||
|
cache.set(f"user_log_update_{request.user.id}", True, 120)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -8,7 +8,6 @@ def render_email_message(request, contexts):
|
||||||
email_contexts = {
|
email_contexts = {
|
||||||
"username": request.user.username,
|
"username": request.user.username,
|
||||||
"domain": current_site.domain,
|
"domain": current_site.domain,
|
||||||
"protocol": "https" if request.is_secure() else "http",
|
|
||||||
"site_name": settings.SITE_NAME,
|
"site_name": settings.SITE_NAME,
|
||||||
"message": None,
|
"message": None,
|
||||||
"title": None,
|
"title": None,
|
||||||
|
|
|
@ -116,7 +116,7 @@ def infinite_paginate(queryset, page, page_size, pad_pages, paginator=None):
|
||||||
|
|
||||||
|
|
||||||
class InfinitePaginationMixin:
|
class InfinitePaginationMixin:
|
||||||
pad_pages = 4
|
pad_pages = 2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def use_infinite_pagination(self):
|
def use_infinite_pagination(self):
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import yaml
|
import yaml
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
@ -48,6 +49,13 @@ class ProblemDataStorage(FileSystemStorage):
|
||||||
def rename(self, old, new):
|
def rename(self, old, new):
|
||||||
return os.rename(self.path(old), self.path(new))
|
return os.rename(self.path(old), self.path(new))
|
||||||
|
|
||||||
|
def delete_directory(self, name):
|
||||||
|
directory_path = self.path(name)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(directory_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProblemDataError(Exception):
|
class ProblemDataError(Exception):
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
|
@ -82,8 +90,8 @@ class ProblemDataCompiler(object):
|
||||||
)
|
)
|
||||||
return custom_checker_path[1]
|
return custom_checker_path[1]
|
||||||
|
|
||||||
if case.checker == "customval":
|
if case.checker == "customcpp":
|
||||||
custom_checker_path = split_path_first(case.custom_validator.name)
|
custom_checker_path = split_path_first(case.custom_checker_cpp.name)
|
||||||
if len(custom_checker_path) != 2:
|
if len(custom_checker_path) != 2:
|
||||||
raise ProblemDataError(
|
raise ProblemDataError(
|
||||||
_("How did you corrupt the custom checker path?")
|
_("How did you corrupt the custom checker path?")
|
||||||
|
@ -98,7 +106,7 @@ class ProblemDataCompiler(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
if case.checker == "testlib":
|
if case.checker == "testlib":
|
||||||
custom_checker_path = split_path_first(case.custom_validator.name)
|
custom_checker_path = split_path_first(case.custom_checker_cpp.name)
|
||||||
if len(custom_checker_path) != 2:
|
if len(custom_checker_path) != 2:
|
||||||
raise ProblemDataError(
|
raise ProblemDataError(
|
||||||
_("How did you corrupt the custom checker path?")
|
_("How did you corrupt the custom checker path?")
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from math import e
|
from math import e
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
import random
|
import random
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
@ -9,6 +10,7 @@ from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When
|
||||||
from django.db.models.fields import FloatField
|
from django.db.models.fields import FloatField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _, gettext_noop
|
from django.utils.translation import gettext as _, gettext_noop
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
from judge.models import Problem, Submission
|
from judge.models import Problem, Submission
|
||||||
from judge.ml.collab_filter import CollabFilter
|
from judge.ml.collab_filter import CollabFilter
|
||||||
|
@ -112,13 +114,21 @@ def _get_result_data(results):
|
||||||
# Using gettext_noop here since this will be tacked into the cache, so it must be language neutral.
|
# Using gettext_noop here since this will be tacked into the cache, so it must be language neutral.
|
||||||
# The caller, SubmissionList.get_result_data will run ugettext on the name.
|
# The caller, SubmissionList.get_result_data will run ugettext on the name.
|
||||||
{"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]},
|
{"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]},
|
||||||
{"code": "WA", "name": gettext_noop("Wrong"), "count": results["WA"]},
|
{
|
||||||
|
"code": "WA",
|
||||||
|
"name": gettext_noop("Wrong Answer"),
|
||||||
|
"count": results["WA"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "CE",
|
"code": "CE",
|
||||||
"name": gettext_noop("Compile Error"),
|
"name": gettext_noop("Compile Error"),
|
||||||
"count": results["CE"],
|
"count": results["CE"],
|
||||||
},
|
},
|
||||||
{"code": "TLE", "name": gettext_noop("Timeout"), "count": results["TLE"]},
|
{
|
||||||
|
"code": "TLE",
|
||||||
|
"name": gettext_noop("Time Limit Exceeded"),
|
||||||
|
"count": results["TLE"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "ERR",
|
"code": "ERR",
|
||||||
"name": gettext_noop("Error"),
|
"name": gettext_noop("Error"),
|
||||||
|
@ -165,7 +175,7 @@ def editable_problems(user, profile=None):
|
||||||
return subquery
|
return subquery
|
||||||
|
|
||||||
|
|
||||||
@cache_wrapper(prefix="hp", timeout=900)
|
@cache_wrapper(prefix="hp", timeout=14400)
|
||||||
def hot_problems(duration, limit):
|
def hot_problems(duration, limit):
|
||||||
qs = Problem.get_public_problems().filter(
|
qs = Problem.get_public_problems().filter(
|
||||||
submission__date__gt=timezone.now() - duration
|
submission__date__gt=timezone.now() - duration
|
||||||
|
@ -222,7 +232,7 @@ def hot_problems(duration, limit):
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
@cache_wrapper(prefix="grp", timeout=26400)
|
@cache_wrapper(prefix="grp", timeout=14400)
|
||||||
def get_related_problems(profile, problem, limit=8):
|
def get_related_problems(profile, problem, limit=8):
|
||||||
if not profile or not settings.ML_OUTPUT_PATH:
|
if not profile or not settings.ML_OUTPUT_PATH:
|
||||||
return None
|
return None
|
||||||
|
@ -248,3 +258,72 @@ def finished_submission(sub):
|
||||||
keys += ["contest_complete:%d" % participation.id]
|
keys += ["contest_complete:%d" % participation.id]
|
||||||
keys += ["contest_attempted:%d" % participation.id]
|
keys += ["contest_attempted:%d" % participation.id]
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendationType(Enum):
|
||||||
|
HOT_PROBLEM = 1
|
||||||
|
CF_DOT = 2
|
||||||
|
CF_COSINE = 3
|
||||||
|
CF_TIME_DOT = 4
|
||||||
|
CF_TIME_COSINE = 5
|
||||||
|
|
||||||
|
|
||||||
|
# Return a list of list. Each inner list correspond to each type in types
|
||||||
|
def get_user_recommended_problems(
|
||||||
|
user_id,
|
||||||
|
problem_ids,
|
||||||
|
recommendation_types,
|
||||||
|
limits,
|
||||||
|
shuffle=False,
|
||||||
|
):
|
||||||
|
cf_model = CollabFilter("collab_filter")
|
||||||
|
cf_time_model = CollabFilter("collab_filter_time")
|
||||||
|
|
||||||
|
def get_problem_ids_from_type(rec_type, limit):
|
||||||
|
if type(rec_type) == int:
|
||||||
|
try:
|
||||||
|
rec_type = RecommendationType(rec_type)
|
||||||
|
except ValueError:
|
||||||
|
raise Http404()
|
||||||
|
if rec_type == RecommendationType.HOT_PROBLEM:
|
||||||
|
return [
|
||||||
|
problem.id
|
||||||
|
for problem in hot_problems(timedelta(days=7), limit)
|
||||||
|
if problem.id in set(problem_ids)
|
||||||
|
]
|
||||||
|
if rec_type == RecommendationType.CF_DOT:
|
||||||
|
return cf_model.user_recommendations(
|
||||||
|
user_id, problem_ids, cf_model.DOT, limit
|
||||||
|
)
|
||||||
|
if rec_type == RecommendationType.CF_COSINE:
|
||||||
|
return cf_model.user_recommendations(
|
||||||
|
user_id, problem_ids, cf_model.COSINE, limit
|
||||||
|
)
|
||||||
|
if rec_type == RecommendationType.CF_TIME_DOT:
|
||||||
|
return cf_time_model.user_recommendations(
|
||||||
|
user_id, problem_ids, cf_model.DOT, limit
|
||||||
|
)
|
||||||
|
if rec_type == RecommendationType.CF_TIME_COSINE:
|
||||||
|
return cf_time_model.user_recommendations(
|
||||||
|
user_id, problem_ids, cf_model.COSINE, limit
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_problems = []
|
||||||
|
for rec_type, limit in zip(recommendation_types, limits):
|
||||||
|
all_problems += get_problem_ids_from_type(rec_type, limit)
|
||||||
|
if shuffle:
|
||||||
|
seed = datetime.now().strftime("%d%m%Y")
|
||||||
|
random.Random(seed).shuffle(all_problems)
|
||||||
|
|
||||||
|
# deduplicate problems
|
||||||
|
res = []
|
||||||
|
used_pid = set()
|
||||||
|
|
||||||
|
for obj in all_problems:
|
||||||
|
if type(obj) == tuple:
|
||||||
|
obj = obj[1]
|
||||||
|
if obj not in used_pid:
|
||||||
|
res.append(obj)
|
||||||
|
used_pid.add(obj)
|
||||||
|
return res
|
||||||
|
|
67
judge/utils/users.py
Normal file
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.utils.translation import ugettext as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from judge.comments import CommentedDetailView
|
from judge.views.comment import CommentedDetailView
|
||||||
from judge.views.pagevote import PageVoteDetailView
|
from judge.views.pagevote import PageVoteDetailView
|
||||||
from judge.views.bookmark import BookMarkDetailView
|
from judge.views.bookmark import BookMarkDetailView
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
|
@ -23,9 +23,9 @@ from judge.models import (
|
||||||
from judge.models.profile import Organization, OrganizationProfile
|
from judge.models.profile import Organization, OrganizationProfile
|
||||||
from judge.utils.cachedict import CacheDict
|
from judge.utils.cachedict import CacheDict
|
||||||
from judge.utils.diggpaginator import DiggPaginator
|
from judge.utils.diggpaginator import DiggPaginator
|
||||||
from judge.utils.problems import user_completed_ids
|
|
||||||
from judge.utils.tickets import filter_visible_tickets
|
from judge.utils.tickets import filter_visible_tickets
|
||||||
from judge.utils.views import TitleMixin
|
from judge.utils.views import TitleMixin
|
||||||
|
from judge.utils.users import get_rating_rank, get_points_rank, get_awards
|
||||||
from judge.views.feed import FeedView
|
from judge.views.feed import FeedView
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,12 +70,37 @@ class HomeFeedView(FeedView):
|
||||||
profile_queryset = Profile.objects
|
profile_queryset = Profile.objects
|
||||||
if self.request.organization:
|
if self.request.organization:
|
||||||
profile_queryset = self.request.organization.members
|
profile_queryset = self.request.organization.members
|
||||||
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by(
|
context["top_rated"] = (
|
||||||
"-rating"
|
profile_queryset.filter(is_unlisted=False)
|
||||||
)[:10]
|
.order_by("-rating")
|
||||||
context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by(
|
.only("id", "rating")[:10]
|
||||||
"-performance_points"
|
)
|
||||||
)[:10]
|
context["top_scorer"] = (
|
||||||
|
profile_queryset.filter(is_unlisted=False)
|
||||||
|
.order_by("-performance_points")
|
||||||
|
.only("id", "performance_points")[:10]
|
||||||
|
)
|
||||||
|
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
|
||||||
|
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
context["rating_rank"] = get_rating_rank(self.request.profile)
|
||||||
|
context["points_rank"] = get_points_rank(self.request.profile)
|
||||||
|
|
||||||
|
medals_list = get_awards(self.request.profile)
|
||||||
|
context["awards"] = {
|
||||||
|
"medals": medals_list,
|
||||||
|
"gold_count": 0,
|
||||||
|
"silver_count": 0,
|
||||||
|
"bronze_count": 0,
|
||||||
|
}
|
||||||
|
for medal in medals_list:
|
||||||
|
if medal["ranking"] == 1:
|
||||||
|
context["awards"]["gold_count"] += 1
|
||||||
|
elif medal["ranking"] == 2:
|
||||||
|
context["awards"]["silver_count"] += 1
|
||||||
|
elif medal["ranking"] == 3:
|
||||||
|
context["awards"]["bronze_count"] += 1
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -91,7 +116,7 @@ class PostList(HomeFeedView):
|
||||||
queryset = (
|
queryset = (
|
||||||
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
|
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
|
||||||
.order_by("-sticky", "-publish_on")
|
.order_by("-sticky", "-publish_on")
|
||||||
.prefetch_related("authors__user", "organizations")
|
.prefetch_related("organizations")
|
||||||
)
|
)
|
||||||
filter = Q(is_organization_private=False)
|
filter = Q(is_organization_private=False)
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
|
@ -126,7 +151,6 @@ class TicketFeed(HomeFeedView):
|
||||||
)
|
)
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.prefetch_related("linked_item")
|
.prefetch_related("linked_item")
|
||||||
.select_related("user__user")
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
@ -137,7 +161,6 @@ class TicketFeed(HomeFeedView):
|
||||||
Ticket.objects.order_by("-id")
|
Ticket.objects.order_by("-id")
|
||||||
.filter(is_open=True)
|
.filter(is_open=True)
|
||||||
.prefetch_related("linked_item")
|
.prefetch_related("linked_item")
|
||||||
.select_related("user__user")
|
|
||||||
)
|
)
|
||||||
return filter_visible_tickets(tickets, self.request.user, profile)
|
return filter_visible_tickets(tickets, self.request.user, profile)
|
||||||
else:
|
else:
|
||||||
|
@ -180,25 +203,24 @@ class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDeta
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(PostView, self).get_context_data(**kwargs)
|
context = super(PostView, self).get_context_data(**kwargs)
|
||||||
context["og_image"] = self.object.og_image
|
context["og_image"] = self.object.og_image
|
||||||
context["valid_user_to_show_edit"] = False
|
context["editable_orgs"] = []
|
||||||
context["valid_org_to_show_edit"] = []
|
|
||||||
|
|
||||||
if self.request.profile in self.object.authors.all():
|
orgs = list(self.object.organizations.all())
|
||||||
context["valid_user_to_show_edit"] = True
|
|
||||||
|
|
||||||
for valid_org_to_show_edit in self.object.organizations.all():
|
if self.request.profile:
|
||||||
if self.request.profile in valid_org_to_show_edit.admins.all():
|
if self.request.profile.id in self.object.get_authors():
|
||||||
context["valid_user_to_show_edit"] = True
|
for org in orgs:
|
||||||
|
if org.is_member(self.request.profile):
|
||||||
if context["valid_user_to_show_edit"]:
|
context["editable_orgs"].append(org)
|
||||||
for post_org in self.object.organizations.all():
|
else:
|
||||||
if post_org in self.request.profile.organizations.all():
|
for org in orgs:
|
||||||
context["valid_org_to_show_edit"].append(post_org)
|
if org.is_admin(self.request.profile):
|
||||||
|
context["editable_orgs"].append(org)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
post = super(PostView, self).get_object(queryset)
|
post = super(PostView, self).get_object(queryset)
|
||||||
if not post.can_see(self.request.user):
|
if not post.is_accessible_by(self.request.user):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
return post
|
return post
|
||||||
|
|
|
@ -8,13 +8,12 @@ from django.http import (
|
||||||
HttpResponseForbidden,
|
HttpResponseForbidden,
|
||||||
)
|
)
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from judge.models.bookmark import BookMark, MakeBookMark
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
from judge.dblock import LockModel
|
|
||||||
from django.views.generic import View, ListView
|
from django.views.generic import View, ListView
|
||||||
|
|
||||||
|
from judge.models.bookmark import BookMark, MakeBookMark, dirty_bookmark
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"dobookmark_page",
|
"dobookmark_page",
|
||||||
|
@ -33,30 +32,31 @@ def bookmark_page(request, delta):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bookmark_id = int(request.POST["id"])
|
bookmark_id = int(request.POST["id"])
|
||||||
bookmark_page = BookMark.objects.filter(id=bookmark_id)
|
bookmark = BookMark.objects.get(id=bookmark_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
else:
|
except BookMark.DoesNotExist:
|
||||||
if not bookmark_page.exists():
|
raise Http404()
|
||||||
raise Http404()
|
|
||||||
|
|
||||||
if delta == 0:
|
if delta == 0:
|
||||||
bookmarklist = MakeBookMark.objects.filter(
|
bookmarklist = MakeBookMark.objects.filter(
|
||||||
bookmark=bookmark_page.first(), user=request.profile
|
bookmark=bookmark, user=request.profile
|
||||||
)
|
)
|
||||||
if not bookmarklist.exists():
|
if not bookmarklist.exists():
|
||||||
newbookmark = MakeBookMark(
|
newbookmark = MakeBookMark(
|
||||||
bookmark=bookmark_page.first(),
|
bookmark=bookmark,
|
||||||
user=request.profile,
|
user=request.profile,
|
||||||
)
|
)
|
||||||
newbookmark.save()
|
newbookmark.save()
|
||||||
else:
|
else:
|
||||||
bookmarklist = MakeBookMark.objects.filter(
|
bookmarklist = MakeBookMark.objects.filter(
|
||||||
bookmark=bookmark_page.first(), user=request.profile
|
bookmark=bookmark, user=request.profile
|
||||||
)
|
)
|
||||||
if bookmarklist.exists():
|
if bookmarklist.exists():
|
||||||
bookmarklist.delete()
|
bookmarklist.delete()
|
||||||
|
|
||||||
|
dirty_bookmark(bookmark, request.profile)
|
||||||
|
|
||||||
return HttpResponse("success", content_type="text/plain")
|
return HttpResponse("success", content_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,46 @@
|
||||||
from django.conf import settings
|
import json
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.context_processors import PermWrapper
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||||
from django.contrib.auth.context_processors import PermWrapper
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q, F, Count, FilteredRelation
|
from django.db.models import Count, F, FilteredRelation, Q
|
||||||
|
from django.db.models.expressions import Value
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.db.models.expressions import F, Value
|
from django.forms import ModelForm
|
||||||
from django.forms.models import ModelForm
|
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404,
|
Http404,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseBadRequest,
|
HttpResponseBadRequest,
|
||||||
HttpResponseForbidden,
|
HttpResponseForbidden,
|
||||||
|
HttpResponseNotFound,
|
||||||
|
HttpResponseRedirect,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.decorators.http import require_POST
|
|
||||||
from django.views.generic import DetailView, UpdateView
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.utils.datastructures import MultiValueDictKeyError
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.views.generic import DetailView, UpdateView, View
|
||||||
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
from django_ratelimit.decorators import ratelimit
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from reversion import revisions
|
from reversion import revisions
|
||||||
from reversion.models import Version
|
from reversion.models import Revision, Version
|
||||||
|
|
||||||
from judge.dblock import LockModel
|
from judge.jinja2.reference import get_user_from_text
|
||||||
from judge.models import Comment, CommentVote, Notification, BlogPost
|
from judge.models import BlogPost, Comment, CommentVote, Notification
|
||||||
|
from judge.models.notification import make_notification
|
||||||
|
from judge.models.comment import get_visible_comment_count
|
||||||
from judge.utils.views import TitleMixin
|
from judge.utils.views import TitleMixin
|
||||||
from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget
|
from judge.widgets import HeavyPreviewPageDownWidget
|
||||||
from judge.comments import add_mention_notifications
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"upvote_comment",
|
"upvote_comment",
|
||||||
|
@ -39,7 +50,20 @@ __all__ = [
|
||||||
"CommentEdit",
|
"CommentEdit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DEFAULT_OFFSET = 10
|
||||||
|
|
||||||
|
|
||||||
|
def _get_html_link_notification(comment):
|
||||||
|
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
|
||||||
|
|
||||||
|
|
||||||
|
def add_mention_notifications(comment):
|
||||||
|
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
|
||||||
|
link = _get_html_link_notification(comment)
|
||||||
|
make_notification(users_mentioned, "Mention", link, comment.author)
|
||||||
|
|
||||||
|
|
||||||
|
@ratelimit(key="user", rate=settings.RL_VOTE)
|
||||||
@login_required
|
@login_required
|
||||||
def vote_comment(request, delta):
|
def vote_comment(request, delta):
|
||||||
if abs(delta) != 1:
|
if abs(delta) != 1:
|
||||||
|
@ -77,27 +101,21 @@ def vote_comment(request, delta):
|
||||||
vote.voter = request.profile
|
vote.voter = request.profile
|
||||||
vote.score = delta
|
vote.score = delta
|
||||||
|
|
||||||
while True:
|
try:
|
||||||
|
vote.save()
|
||||||
|
except IntegrityError:
|
||||||
try:
|
try:
|
||||||
vote.save()
|
vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile)
|
||||||
except IntegrityError:
|
except CommentVote.DoesNotExist:
|
||||||
with LockModel(write=(CommentVote,)):
|
raise Http404()
|
||||||
try:
|
if -vote.score != delta:
|
||||||
vote = CommentVote.objects.get(
|
return HttpResponseBadRequest(
|
||||||
comment_id=comment_id, voter=request.profile
|
_("You already voted."), content_type="text/plain"
|
||||||
)
|
)
|
||||||
except CommentVote.DoesNotExist:
|
vote.delete()
|
||||||
# We must continue racing in case this is exploited to manipulate votes.
|
Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
|
||||||
continue
|
else:
|
||||||
if -vote.score != delta:
|
Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
|
||||||
return HttpResponseBadRequest(
|
|
||||||
_("You already voted."), content_type="text/plain"
|
|
||||||
)
|
|
||||||
vote.delete()
|
|
||||||
Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
|
|
||||||
else:
|
|
||||||
Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
|
|
||||||
break
|
|
||||||
return HttpResponse("success", content_type="text/plain")
|
return HttpResponse("success", content_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,7 +131,7 @@ def get_comments(request, limit=10):
|
||||||
try:
|
try:
|
||||||
comment_id = int(request.GET["id"])
|
comment_id = int(request.GET["id"])
|
||||||
parent_none = int(request.GET["parent_none"])
|
parent_none = int(request.GET["parent_none"])
|
||||||
except ValueError:
|
except (ValueError, MultiValueDictKeyError):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
else:
|
else:
|
||||||
if comment_id and not Comment.objects.filter(id=comment_id).exists():
|
if comment_id and not Comment.objects.filter(id=comment_id).exists():
|
||||||
|
@ -121,7 +139,10 @@ def get_comments(request, limit=10):
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
if "offset" in request.GET:
|
if "offset" in request.GET:
|
||||||
offset = int(request.GET["offset"])
|
try:
|
||||||
|
offset = int(request.GET["offset"])
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
target_comment = -1
|
target_comment = -1
|
||||||
if "target_comment" in request.GET:
|
if "target_comment" in request.GET:
|
||||||
|
@ -147,7 +168,6 @@ def get_comments(request, limit=10):
|
||||||
.defer("author__about")
|
.defer("author__about")
|
||||||
.annotate(
|
.annotate(
|
||||||
count_replies=Count("replies", distinct=True),
|
count_replies=Count("replies", distinct=True),
|
||||||
revisions=Count("versions", distinct=True),
|
|
||||||
)[offset : offset + limit]
|
)[offset : offset + limit]
|
||||||
)
|
)
|
||||||
profile = None
|
profile = None
|
||||||
|
@ -241,8 +261,9 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
|
||||||
# update notifications
|
# update notifications
|
||||||
comment = form.instance
|
comment = form.instance
|
||||||
add_mention_notifications(comment)
|
add_mention_notifications(comment)
|
||||||
|
comment.revision_count = comment.versions.count() + 1
|
||||||
with transaction.atomic(), revisions.create_revision():
|
comment.save(update_fields=["revision_count"])
|
||||||
|
with revisions.create_revision():
|
||||||
revisions.set_comment(_("Edited from site"))
|
revisions.set_comment(_("Edited from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
return super(CommentEditAjax, self).form_valid(form)
|
return super(CommentEditAjax, self).form_valid(form)
|
||||||
|
@ -294,4 +315,195 @@ def comment_hide(request):
|
||||||
|
|
||||||
comment = get_object_or_404(Comment, id=comment_id)
|
comment = get_object_or_404(Comment, id=comment_id)
|
||||||
comment.get_descendants(include_self=True).update(hidden=True)
|
comment.get_descendants(include_self=True).update(hidden=True)
|
||||||
|
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
|
||||||
return HttpResponse("ok")
|
return HttpResponse("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ["body", "parent"]
|
||||||
|
widgets = {
|
||||||
|
"parent": forms.HiddenInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
|
widgets["body"] = HeavyPreviewPageDownWidget(
|
||||||
|
preview=reverse_lazy("comment_preview"),
|
||||||
|
preview_timeout=1000,
|
||||||
|
hide_preview_button=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
super(CommentForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.request is not None and self.request.user.is_authenticated:
|
||||||
|
profile = self.request.profile
|
||||||
|
if profile.mute:
|
||||||
|
raise ValidationError(_("Your part is silent, little toad."))
|
||||||
|
elif (
|
||||||
|
not self.request.user.is_staff
|
||||||
|
and not profile.submission_set.filter(
|
||||||
|
points=F("problem__points")
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"You need to have solved at least one problem "
|
||||||
|
"before your voice can be heard."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return super(CommentForm, self).clean()
|
||||||
|
|
||||||
|
|
||||||
|
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
||||||
|
comment_page = None
|
||||||
|
|
||||||
|
def is_comment_locked(self):
|
||||||
|
if self.request.user.has_perm("judge.override_comment_lock"):
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
self.request.in_contest
|
||||||
|
and self.request.participation.contest.use_clarifications
|
||||||
|
)
|
||||||
|
|
||||||
|
@method_decorator(ratelimit(key="user", rate=settings.RL_COMMENT))
|
||||||
|
@method_decorator(login_required)
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
if self.is_comment_locked():
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
parent = request.POST.get("parent")
|
||||||
|
if parent:
|
||||||
|
try:
|
||||||
|
parent = int(parent)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
else:
|
||||||
|
if not self.object.comments.filter(hidden=False, id=parent).exists():
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
form = CommentForm(request, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
comment = form.save(commit=False)
|
||||||
|
comment.author = request.profile
|
||||||
|
comment.linked_object = self.object
|
||||||
|
|
||||||
|
with revisions.create_revision():
|
||||||
|
revisions.set_user(request.user)
|
||||||
|
revisions.set_comment(_("Posted comment"))
|
||||||
|
comment.save()
|
||||||
|
|
||||||
|
# add notification for reply
|
||||||
|
comment_notif_link = _get_html_link_notification(comment)
|
||||||
|
if comment.parent and comment.parent.author != comment.author:
|
||||||
|
make_notification(
|
||||||
|
[comment.parent.author], "Reply", comment_notif_link, comment.author
|
||||||
|
)
|
||||||
|
|
||||||
|
# add notification for page authors
|
||||||
|
page_authors = comment.linked_object.authors.all()
|
||||||
|
make_notification(
|
||||||
|
page_authors, "Comment", comment_notif_link, comment.author
|
||||||
|
)
|
||||||
|
|
||||||
|
add_mention_notifications(comment)
|
||||||
|
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(comment.get_absolute_url())
|
||||||
|
|
||||||
|
context = self.get_context_data(object=self.object, comment_form=form)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
target_comment = None
|
||||||
|
self.object = self.get_object()
|
||||||
|
if "comment-id" in request.GET:
|
||||||
|
try:
|
||||||
|
comment_id = int(request.GET["comment-id"])
|
||||||
|
comment_obj = Comment.objects.get(id=comment_id)
|
||||||
|
except (Comment.DoesNotExist, ValueError):
|
||||||
|
raise Http404
|
||||||
|
if comment_obj.linked_object != self.object:
|
||||||
|
raise Http404
|
||||||
|
target_comment = comment_obj.get_root()
|
||||||
|
return self.render_to_response(
|
||||||
|
self.get_context_data(
|
||||||
|
object=self.object,
|
||||||
|
target_comment=target_comment,
|
||||||
|
comment_form=CommentForm(request, initial={"parent": None}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_queryset(self, target_comment):
|
||||||
|
if target_comment:
|
||||||
|
queryset = target_comment.get_descendants(include_self=True)
|
||||||
|
queryset = queryset.filter(hidden=False)
|
||||||
|
else:
|
||||||
|
queryset = self.object.comments
|
||||||
|
queryset = queryset.filter(parent=None, hidden=False)
|
||||||
|
queryset = queryset.filter(hidden=False).annotate(
|
||||||
|
count_replies=Count("replies", distinct=True),
|
||||||
|
)[:DEFAULT_OFFSET]
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
profile = self.request.profile
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
my_vote=FilteredRelation(
|
||||||
|
"votes", condition=Q(votes__voter_id=profile.id)
|
||||||
|
),
|
||||||
|
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, target_comment=None, **kwargs):
|
||||||
|
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
||||||
|
queryset = self._get_queryset(target_comment)
|
||||||
|
comment_count = self.object.comments.filter(parent=None, hidden=False).count()
|
||||||
|
|
||||||
|
content_type = ContentType.objects.get_for_model(self.object)
|
||||||
|
all_comment_count = get_visible_comment_count(content_type, self.object.pk)
|
||||||
|
|
||||||
|
if target_comment != None:
|
||||||
|
context["target_comment"] = target_comment.id
|
||||||
|
else:
|
||||||
|
context["target_comment"] = -1
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
context["is_new_user"] = (
|
||||||
|
not self.request.user.is_staff
|
||||||
|
and not self.request.profile.submission_set.filter(
|
||||||
|
points=F("problem__points")
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
context["comment_lock"] = self.is_comment_locked()
|
||||||
|
context["comment_list"] = list(queryset)
|
||||||
|
context["has_comments"] = len(context["comment_list"]) > 0
|
||||||
|
|
||||||
|
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
||||||
|
|
||||||
|
if queryset.exists():
|
||||||
|
context["comment_root_id"] = context["comment_list"][0].id
|
||||||
|
else:
|
||||||
|
context["comment_root_id"] = 0
|
||||||
|
|
||||||
|
context["comment_parent_none"] = 1
|
||||||
|
|
||||||
|
if target_comment != None:
|
||||||
|
context["offset"] = 0
|
||||||
|
context["comment_more"] = comment_count - 1
|
||||||
|
else:
|
||||||
|
context["offset"] = DEFAULT_OFFSET
|
||||||
|
context["comment_more"] = comment_count - DEFAULT_OFFSET
|
||||||
|
|
||||||
|
context["limit"] = DEFAULT_OFFSET
|
||||||
|
context["comment_count"] = comment_count
|
||||||
|
context["profile"] = self.request.profile
|
||||||
|
context["all_comment_count"] = all_comment_count
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
|
@ -27,7 +27,6 @@ from django.db.models import (
|
||||||
Value,
|
Value,
|
||||||
When,
|
When,
|
||||||
)
|
)
|
||||||
from django.db.models.signals import post_save, post_delete
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models.expressions import CombinedExpression
|
from django.db.models.expressions import CombinedExpression
|
||||||
from django.http import (
|
from django.http import (
|
||||||
|
@ -56,7 +55,7 @@ from django.views.generic.detail import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from judge import event_poster as event
|
from judge import event_poster as event
|
||||||
from judge.comments import CommentedDetailView
|
from judge.views.comment import CommentedDetailView
|
||||||
from judge.forms import ContestCloneForm
|
from judge.forms import ContestCloneForm
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
Contest,
|
Contest,
|
||||||
|
@ -70,6 +69,8 @@ from judge.models import (
|
||||||
Submission,
|
Submission,
|
||||||
ContestProblemClarification,
|
ContestProblemClarification,
|
||||||
ContestsSummary,
|
ContestsSummary,
|
||||||
|
OfficialContestCategory,
|
||||||
|
OfficialContestLocation,
|
||||||
)
|
)
|
||||||
from judge.tasks import run_moss
|
from judge.tasks import run_moss
|
||||||
from judge.utils.celery import redirect_to_task_status
|
from judge.utils.celery import redirect_to_task_status
|
||||||
|
@ -107,6 +108,7 @@ __all__ = [
|
||||||
"base_contest_ranking_list",
|
"base_contest_ranking_list",
|
||||||
"ContestClarificationView",
|
"ContestClarificationView",
|
||||||
"update_contest_mode",
|
"update_contest_mode",
|
||||||
|
"OfficialContestList",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,8 +132,17 @@ def _find_contest(request, key):
|
||||||
|
|
||||||
|
|
||||||
class ContestListMixin(object):
|
class ContestListMixin(object):
|
||||||
|
official = False
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Contest.get_visible_contests(self.request.user)
|
q = Contest.get_visible_contests(self.request.user)
|
||||||
|
if self.official:
|
||||||
|
q = q.filter(official__isnull=False).select_related(
|
||||||
|
"official", "official__category", "official__location"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
q = q.filter(official__isnull=True)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
class ContestList(
|
class ContestList(
|
||||||
|
@ -141,119 +152,190 @@ class ContestList(
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
template_name = "contest/list.html"
|
template_name = "contest/list.html"
|
||||||
title = gettext_lazy("Contests")
|
title = gettext_lazy("Contests")
|
||||||
context_object_name = "past_contests"
|
|
||||||
all_sorts = frozenset(("name", "user_count", "start_time"))
|
all_sorts = frozenset(("name", "user_count", "start_time"))
|
||||||
default_desc = frozenset(("name", "user_count"))
|
default_desc = frozenset(("name", "user_count"))
|
||||||
default_sort = "-start_time"
|
context_object_name = "contests"
|
||||||
|
|
||||||
|
def get_default_sort_order(self, request):
|
||||||
|
if request.GET.get("contest") and settings.ENABLE_FTS:
|
||||||
|
return "-relevance"
|
||||||
|
if self.current_tab == "future":
|
||||||
|
return "start_time"
|
||||||
|
return "-start_time"
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _now(self):
|
def _now(self):
|
||||||
return timezone.now()
|
return timezone.now()
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def GET_with_session(self, request, key):
|
||||||
self.contest_query = None
|
if not request.GET.get(key):
|
||||||
self.org_query = []
|
return request.session.get(key, False)
|
||||||
self.show_orgs = 0
|
return request.GET.get(key, None) == "1"
|
||||||
if request.GET.get("show_orgs"):
|
|
||||||
self.show_orgs = 1
|
|
||||||
|
|
||||||
if "orgs" in self.request.GET and self.request.profile:
|
def setup_contest_list(self, request):
|
||||||
|
self.contest_query = request.GET.get("contest", "")
|
||||||
|
|
||||||
|
self.hide_organization_contests = 0
|
||||||
|
if self.GET_with_session(request, "hide_organization_contests"):
|
||||||
|
self.hide_organization_contests = 1
|
||||||
|
|
||||||
|
self.org_query = []
|
||||||
|
if request.GET.get("orgs") and request.profile:
|
||||||
try:
|
try:
|
||||||
self.org_query = list(map(int, request.GET.getlist("orgs")))
|
self.org_query = list(map(int, request.GET.getlist("orgs")))
|
||||||
if not self.request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
self.org_query = [
|
self.org_query = [
|
||||||
i
|
i
|
||||||
for i in self.org_query
|
for i in self.org_query
|
||||||
if i
|
if i
|
||||||
in self.request.profile.organizations.values_list(
|
in set(
|
||||||
"id", flat=True
|
request.profile.organizations.values_list("id", flat=True)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
default_tab = "active"
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
default_tab = "current"
|
||||||
|
self.current_tab = self.request.GET.get("tab", default_tab)
|
||||||
|
|
||||||
|
self.setup_contest_list(request)
|
||||||
|
|
||||||
return super(ContestList, self).get(request, *args, **kwargs)
|
return super(ContestList, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
to_update = ("hide_organization_contests",)
|
||||||
|
for key in to_update:
|
||||||
|
if key in request.GET:
|
||||||
|
val = request.GET.get(key) == "1"
|
||||||
|
request.session[key] = val
|
||||||
|
else:
|
||||||
|
request.session[key] = False
|
||||||
|
return HttpResponseRedirect(request.get_full_path())
|
||||||
|
|
||||||
|
def extra_queryset_filters(self, queryset):
|
||||||
|
return queryset
|
||||||
|
|
||||||
def _get_queryset(self):
|
def _get_queryset(self):
|
||||||
queryset = (
|
queryset = (
|
||||||
super(ContestList, self)
|
super(ContestList, self)
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.prefetch_related("tags", "organizations", "authors", "curators", "testers")
|
.prefetch_related("tags", "organizations")
|
||||||
)
|
)
|
||||||
|
|
||||||
if "contest" in self.request.GET:
|
if self.contest_query:
|
||||||
self.contest_query = query = " ".join(
|
substr_queryset = queryset.filter(
|
||||||
self.request.GET.getlist("contest")
|
Q(key__icontains=self.contest_query)
|
||||||
).strip()
|
| Q(name__icontains=self.contest_query)
|
||||||
if query:
|
)
|
||||||
substr_queryset = queryset.filter(
|
if settings.ENABLE_FTS:
|
||||||
Q(key__icontains=query) | Q(name__icontains=query)
|
queryset = (
|
||||||
|
queryset.search(self.contest_query).extra(order_by=["-relevance"])
|
||||||
|
| substr_queryset
|
||||||
)
|
)
|
||||||
if settings.ENABLE_FTS:
|
else:
|
||||||
queryset = (
|
queryset = substr_queryset
|
||||||
queryset.search(query).extra(order_by=["-relevance"])
|
|
||||||
| substr_queryset
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
queryset = substr_queryset
|
|
||||||
if not self.org_query and self.request.organization:
|
if not self.org_query and self.request.organization:
|
||||||
self.org_query = [self.request.organization.id]
|
self.org_query = [self.request.organization.id]
|
||||||
if self.show_orgs:
|
if self.hide_organization_contests:
|
||||||
queryset = queryset.filter(organizations=None)
|
queryset = queryset.filter(organizations=None)
|
||||||
if self.org_query:
|
if self.org_query:
|
||||||
queryset = queryset.filter(organizations__in=self.org_query)
|
queryset = queryset.filter(organizations__in=self.org_query)
|
||||||
|
queryset = self.extra_queryset_filters(queryset)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_queryset(self):
|
def _get_past_contests_queryset(self):
|
||||||
return (
|
return (
|
||||||
self._get_queryset()
|
self._get_queryset()
|
||||||
.order_by(self.order, "key")
|
|
||||||
.filter(end_time__lt=self._now)
|
.filter(end_time__lt=self._now)
|
||||||
|
.order_by(self.order, "key")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _active_participations(self):
|
||||||
|
return ContestParticipation.objects.filter(
|
||||||
|
virtual=0,
|
||||||
|
user=self.request.profile,
|
||||||
|
contest__start_time__lte=self._now,
|
||||||
|
contest__end_time__gte=self._now,
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _active_contests_ids(self):
|
||||||
|
return [
|
||||||
|
participation.contest_id
|
||||||
|
for participation in self._active_participations().select_related("contest")
|
||||||
|
if not participation.ended
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_current_contests_queryset(self):
|
||||||
|
return (
|
||||||
|
self._get_queryset()
|
||||||
|
.exclude(id__in=self._active_contests_ids)
|
||||||
|
.filter(start_time__lte=self._now, end_time__gte=self._now)
|
||||||
|
.order_by(self.order, "key")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_future_contests_queryset(self):
|
||||||
|
return (
|
||||||
|
self._get_queryset()
|
||||||
|
.filter(start_time__gt=self._now)
|
||||||
|
.order_by(self.order, "key")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_active_participations_queryset(self):
|
||||||
|
active_contests = (
|
||||||
|
self._get_queryset()
|
||||||
|
.filter(id__in=self._active_contests_ids)
|
||||||
|
.order_by(self.order, "key")
|
||||||
|
)
|
||||||
|
ordered_ids = list(active_contests.values_list("id", flat=True))
|
||||||
|
|
||||||
|
participations = self._active_participations().filter(
|
||||||
|
contest_id__in=ordered_ids
|
||||||
|
)
|
||||||
|
participations = sorted(
|
||||||
|
participations, key=lambda p: ordered_ids.index(p.contest_id)
|
||||||
|
)
|
||||||
|
return participations
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if self.current_tab == "past":
|
||||||
|
return self._get_past_contests_queryset()
|
||||||
|
elif self.current_tab == "current":
|
||||||
|
return self._get_current_contests_queryset()
|
||||||
|
elif self.current_tab == "future":
|
||||||
|
return self._get_future_contests_queryset()
|
||||||
|
else: # Default to active
|
||||||
|
return self._get_active_participations_queryset()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(ContestList, self).get_context_data(**kwargs)
|
context = super(ContestList, self).get_context_data(**kwargs)
|
||||||
present, active, future = [], [], []
|
|
||||||
for contest in self._get_queryset().exclude(end_time__lt=self._now):
|
|
||||||
if contest.start_time > self._now:
|
|
||||||
future.append(contest)
|
|
||||||
else:
|
|
||||||
present.append(contest)
|
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
context["current_tab"] = self.current_tab
|
||||||
for participation in (
|
|
||||||
ContestParticipation.objects.filter(
|
context["current_count"] = self._get_current_contests_queryset().count()
|
||||||
virtual=0, user=self.request.profile, contest_id__in=present
|
context["future_count"] = self._get_future_contests_queryset().count()
|
||||||
)
|
context["active_count"] = len(self._get_active_participations_queryset())
|
||||||
.select_related("contest")
|
|
||||||
.prefetch_related(
|
|
||||||
"contest__authors", "contest__curators", "contest__testers"
|
|
||||||
)
|
|
||||||
.annotate(key=F("contest__key"))
|
|
||||||
):
|
|
||||||
if not participation.ended:
|
|
||||||
active.append(participation)
|
|
||||||
present.remove(participation.contest)
|
|
||||||
|
|
||||||
if not ("contest" in self.request.GET and settings.ENABLE_FTS):
|
|
||||||
active.sort(key=attrgetter("end_time", "key"))
|
|
||||||
present.sort(key=attrgetter("end_time", "key"))
|
|
||||||
future.sort(key=attrgetter("start_time"))
|
|
||||||
context["active_participations"] = active
|
|
||||||
context["current_contests"] = present
|
|
||||||
context["future_contests"] = future
|
|
||||||
context["now"] = self._now
|
context["now"] = self._now
|
||||||
context["first_page_href"] = "."
|
context["first_page_href"] = "."
|
||||||
context["contest_query"] = self.contest_query
|
context["contest_query"] = self.contest_query
|
||||||
context["org_query"] = self.org_query
|
context["org_query"] = self.org_query
|
||||||
context["show_orgs"] = int(self.show_orgs)
|
context["hide_organization_contests"] = int(self.hide_organization_contests)
|
||||||
if self.request.profile:
|
if self.request.profile:
|
||||||
if self.request.user.is_superuser:
|
context["organizations"] = self.request.profile.organizations.all()
|
||||||
context["organizations"] = Organization.objects.all()
|
|
||||||
else:
|
|
||||||
context["organizations"] = self.request.profile.organizations.all()
|
|
||||||
context["page_type"] = "list"
|
context["page_type"] = "list"
|
||||||
|
context["selected_order"] = self.request.GET.get("order")
|
||||||
|
context["all_sort_options"] = [
|
||||||
|
("start_time", _("Start time (asc.)")),
|
||||||
|
("-start_time", _("Start time (desc.)")),
|
||||||
|
("name", _("Name (asc.)")),
|
||||||
|
("-name", _("Name (desc.)")),
|
||||||
|
("user_count", _("User count (asc.)")),
|
||||||
|
("-user_count", _("User count (desc.)")),
|
||||||
|
]
|
||||||
context.update(self.get_sort_context())
|
context.update(self.get_sort_context())
|
||||||
context.update(self.get_sort_paginate_context())
|
context.update(self.get_sort_paginate_context())
|
||||||
return context
|
return context
|
||||||
|
@ -346,6 +428,19 @@ class ContestMixin(object):
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def contest_access_check(self, contest):
|
||||||
|
try:
|
||||||
|
contest.access_check(self.request.user)
|
||||||
|
except Contest.PrivateContest:
|
||||||
|
raise PrivateContestError(
|
||||||
|
contest.name,
|
||||||
|
contest.is_private,
|
||||||
|
contest.is_organization_private,
|
||||||
|
contest.organizations.all(),
|
||||||
|
)
|
||||||
|
except Contest.Inaccessible:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
contest = super(ContestMixin, self).get_object(queryset)
|
contest = super(ContestMixin, self).get_object(queryset)
|
||||||
profile = self.request.profile
|
profile = self.request.profile
|
||||||
|
@ -361,19 +456,9 @@ class ContestMixin(object):
|
||||||
if self.should_bypass_access_check(contest):
|
if self.should_bypass_access_check(contest):
|
||||||
return contest
|
return contest
|
||||||
|
|
||||||
try:
|
self.contest_access_check(contest)
|
||||||
contest.access_check(self.request.user)
|
|
||||||
except Contest.PrivateContest:
|
return contest
|
||||||
raise PrivateContestError(
|
|
||||||
contest.name,
|
|
||||||
contest.is_private,
|
|
||||||
contest.is_organization_private,
|
|
||||||
contest.organizations.all(),
|
|
||||||
)
|
|
||||||
except Contest.Inaccessible:
|
|
||||||
raise Http404()
|
|
||||||
else:
|
|
||||||
return contest
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
@ -449,6 +534,10 @@ class ContestDetail(
|
||||||
)
|
)
|
||||||
context["editable_organizations"] = self.get_editable_organizations()
|
context["editable_organizations"] = self.get_editable_organizations()
|
||||||
context["is_clonable"] = is_contest_clonable(self.request, self.object)
|
context["is_clonable"] = is_contest_clonable(self.request, self.object)
|
||||||
|
if self.request.in_contest:
|
||||||
|
context["current_contest"] = self.request.participation.contest
|
||||||
|
else:
|
||||||
|
context["current_contest"] = None
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -459,7 +548,12 @@ def is_contest_clonable(request, contest):
|
||||||
return False
|
return False
|
||||||
if request.user.has_perm("judge.clone_contest"):
|
if request.user.has_perm("judge.clone_contest"):
|
||||||
return True
|
return True
|
||||||
if contest.ended:
|
if contest.access_code and not contest.is_editable_by(request.user):
|
||||||
|
return False
|
||||||
|
if (
|
||||||
|
contest.end_time is not None
|
||||||
|
and contest.end_time + timedelta(days=1) < contest._now
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -498,6 +592,7 @@ class ContestClone(ContestMixin, TitleMixin, SingleObjectFormView):
|
||||||
contest.is_visible = False
|
contest.is_visible = False
|
||||||
contest.user_count = 0
|
contest.user_count = 0
|
||||||
contest.key = form.cleaned_data["key"]
|
contest.key = form.cleaned_data["key"]
|
||||||
|
contest.is_rated = False
|
||||||
contest.save()
|
contest.save()
|
||||||
|
|
||||||
contest.tags.set(tags)
|
contest.tags.set(tags)
|
||||||
|
@ -562,12 +657,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
||||||
|
|
||||||
profile = request.profile
|
profile = request.profile
|
||||||
if profile.current_contest is not None:
|
if profile.current_contest is not None:
|
||||||
return generic_message(
|
profile.remove_contest()
|
||||||
request,
|
|
||||||
_("Already in contest"),
|
|
||||||
_('You are already in a contest: "%s".')
|
|
||||||
% profile.current_contest.contest.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not request.user.is_superuser
|
not request.user.is_superuser
|
||||||
|
@ -646,6 +736,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
||||||
profile.save()
|
profile.save()
|
||||||
contest._updating_stats_only = True
|
contest._updating_stats_only = True
|
||||||
contest.update_user_count()
|
contest.update_user_count()
|
||||||
|
request.session["contest_mode"] = True
|
||||||
return HttpResponseRedirect(reverse("problem_list"))
|
return HttpResponseRedirect(reverse("problem_list"))
|
||||||
|
|
||||||
def ask_for_access_code(self, form=None):
|
def ask_for_access_code(self, form=None):
|
||||||
|
@ -882,7 +973,10 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
|
||||||
if (point == None) or (problem_code not in codes):
|
if (point == None) or (problem_code not in codes):
|
||||||
continue
|
continue
|
||||||
problem_idx = codes.index(problem_code)
|
problem_idx = codes.index(problem_code)
|
||||||
bin_idx = math.floor(point * self.POINT_BIN / max_point)
|
if max_point > 0:
|
||||||
|
bin_idx = math.floor(point * self.POINT_BIN / max_point)
|
||||||
|
else:
|
||||||
|
bin_idx = 0
|
||||||
bin_idx = max(min(bin_idx, self.POINT_BIN), 0)
|
bin_idx = max(min(bin_idx, self.POINT_BIN), 0)
|
||||||
counter[problem_idx][bin_idx] += count
|
counter[problem_idx][bin_idx] += count
|
||||||
for i in range(num_problems):
|
for i in range(num_problems):
|
||||||
|
@ -936,7 +1030,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
|
||||||
|
|
||||||
ContestRankingProfile = namedtuple(
|
ContestRankingProfile = namedtuple(
|
||||||
"ContestRankingProfile",
|
"ContestRankingProfile",
|
||||||
"id user css_class username points cumtime tiebreaker organization participation "
|
"id user username points cumtime tiebreaker participation "
|
||||||
"participation_rating problem_cells result_cell",
|
"participation_rating problem_cells result_cell",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -956,13 +1050,11 @@ def make_contest_ranking_profile(
|
||||||
user = participation.user
|
user = participation.user
|
||||||
return ContestRankingProfile(
|
return ContestRankingProfile(
|
||||||
id=user.id,
|
id=user.id,
|
||||||
user=user.user,
|
user=user,
|
||||||
css_class=user.css_class,
|
|
||||||
username=user.username,
|
username=user.username,
|
||||||
points=points,
|
points=points,
|
||||||
cumtime=cumtime,
|
cumtime=cumtime,
|
||||||
tiebreaker=participation.tiebreaker,
|
tiebreaker=participation.tiebreaker,
|
||||||
organization=user.organization,
|
|
||||||
participation_rating=participation.rating.rating
|
participation_rating=participation.rating.rating
|
||||||
if hasattr(participation, "rating")
|
if hasattr(participation, "rating")
|
||||||
else None,
|
else None,
|
||||||
|
@ -979,45 +1071,60 @@ def make_contest_ranking_profile(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def base_contest_ranking_list(contest, problems, queryset, show_final=False):
|
def base_contest_ranking_list(
|
||||||
return [
|
contest, problems, queryset, show_final=False, extra_participation=None
|
||||||
make_contest_ranking_profile(contest, participation, problems, show_final)
|
):
|
||||||
for participation in queryset.select_related("user__user", "rating").defer(
|
participation_fields = [
|
||||||
"user__about", "user__organizations__about"
|
field.name
|
||||||
)
|
for field in ContestParticipation._meta.get_fields()
|
||||||
|
if field.concrete and not field.many_to_many
|
||||||
|
]
|
||||||
|
fields_to_fetch = participation_fields + [
|
||||||
|
"user__id",
|
||||||
|
"rating__rating",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
res = [
|
||||||
|
make_contest_ranking_profile(contest, participation, problems, show_final)
|
||||||
|
for participation in queryset.select_related("user", "rating").only(
|
||||||
|
*fields_to_fetch
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Profile.prefetch_profile_cache([p.id for p in res])
|
||||||
|
return res
|
||||||
|
|
||||||
def contest_ranking_list(contest, problems, queryset=None, show_final=False):
|
|
||||||
if not queryset:
|
def contest_ranking_list(
|
||||||
|
contest, problems, queryset=None, show_final=False, extra_participation=None
|
||||||
|
):
|
||||||
|
if queryset is None:
|
||||||
queryset = contest.users.filter(virtual=0)
|
queryset = contest.users.filter(virtual=0)
|
||||||
|
|
||||||
if not show_final:
|
if extra_participation and extra_participation.virtual:
|
||||||
return base_contest_ranking_list(
|
queryset = queryset | contest.users.filter(id=extra_participation.id)
|
||||||
contest,
|
|
||||||
problems,
|
if show_final:
|
||||||
queryset.prefetch_related("user__organizations")
|
queryset = queryset.order_by(
|
||||||
.extra(select={"round_score": "round(score, 6)"})
|
"is_disqualified", "-score_final", "cumtime_final", "tiebreaker"
|
||||||
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
|
|
||||||
show_final,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return base_contest_ranking_list(
|
queryset = queryset.order_by(
|
||||||
contest,
|
"is_disqualified", "-score", "cumtime", "tiebreaker"
|
||||||
problems,
|
|
||||||
queryset.prefetch_related("user__organizations")
|
|
||||||
.extra(select={"round_score": "round(score_final, 6)"})
|
|
||||||
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"),
|
|
||||||
show_final,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return base_contest_ranking_list(
|
||||||
|
contest,
|
||||||
|
problems,
|
||||||
|
queryset,
|
||||||
|
show_final,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_contest_ranking_list(
|
def get_contest_ranking_list(
|
||||||
request,
|
request,
|
||||||
contest,
|
contest,
|
||||||
participation=None,
|
participation=None,
|
||||||
ranking_list=contest_ranking_list,
|
ranking_list=contest_ranking_list,
|
||||||
show_current_virtual=False,
|
|
||||||
ranker=ranker,
|
ranker=ranker,
|
||||||
show_final=False,
|
show_final=False,
|
||||||
):
|
):
|
||||||
|
@ -1027,21 +1134,17 @@ def get_contest_ranking_list(
|
||||||
.order_by("order")
|
.order_by("order")
|
||||||
)
|
)
|
||||||
|
|
||||||
users = ranker(
|
if participation is None:
|
||||||
ranking_list(contest, problems, show_final=show_final),
|
participation = _get_current_virtual_participation(request, contest)
|
||||||
key=attrgetter("points", "cumtime", "tiebreaker"),
|
|
||||||
|
ranking_list_result = ranking_list(
|
||||||
|
contest, problems, show_final=show_final, extra_participation=participation
|
||||||
)
|
)
|
||||||
|
|
||||||
if show_current_virtual:
|
users = ranker(
|
||||||
if participation is None and request.user.is_authenticated:
|
ranking_list_result,
|
||||||
participation = request.profile.current_contest
|
key=attrgetter("points", "cumtime", "tiebreaker"),
|
||||||
if participation is None or participation.contest_id != contest.id:
|
)
|
||||||
participation = None
|
|
||||||
if participation is not None and participation.virtual:
|
|
||||||
users = chain(
|
|
||||||
[("-", make_contest_ranking_profile(contest, participation, problems))],
|
|
||||||
users,
|
|
||||||
)
|
|
||||||
return users, problems
|
return users, problems
|
||||||
|
|
||||||
|
|
||||||
|
@ -1061,6 +1164,9 @@ def contest_ranking_ajax(request, contest, participation=None):
|
||||||
):
|
):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
|
if participation is None:
|
||||||
|
participation = _get_current_virtual_participation(request, contest)
|
||||||
|
|
||||||
queryset = contest.users.filter(virtual__gte=0)
|
queryset = contest.users.filter(virtual__gte=0)
|
||||||
if request.GET.get("friend") == "true" and request.profile:
|
if request.GET.get("friend") == "true" and request.profile:
|
||||||
friends = request.profile.get_friends()
|
friends = request.profile.get_friends()
|
||||||
|
@ -1072,7 +1178,9 @@ def contest_ranking_ajax(request, contest, participation=None):
|
||||||
request,
|
request,
|
||||||
contest,
|
contest,
|
||||||
participation,
|
participation,
|
||||||
ranking_list=partial(contest_ranking_list, queryset=queryset),
|
ranking_list=partial(
|
||||||
|
contest_ranking_list, queryset=queryset, extra_participation=participation
|
||||||
|
),
|
||||||
show_final=show_final,
|
show_final=show_final,
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
|
@ -1088,6 +1196,19 @@ def contest_ranking_ajax(request, contest, participation=None):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_virtual_participation(request, contest):
|
||||||
|
# Return None if not eligible
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return None
|
||||||
|
|
||||||
|
participation = request.profile.current_contest
|
||||||
|
|
||||||
|
if participation is None or participation.contest_id != contest.id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return participation
|
||||||
|
|
||||||
|
|
||||||
class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
|
class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
|
||||||
template_name = "contest/ranking.html"
|
template_name = "contest/ranking.html"
|
||||||
page_type = None
|
page_type = None
|
||||||
|
@ -1182,7 +1303,6 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
|
||||||
return get_contest_ranking_list(
|
return get_contest_ranking_list(
|
||||||
self.request,
|
self.request,
|
||||||
self.object,
|
self.object,
|
||||||
show_current_virtual=False,
|
|
||||||
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
|
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
|
||||||
ranker=lambda users, key: (
|
ranker=lambda users, key: (
|
||||||
(user.participation.virtual or live_link, user) for user in users
|
(user.participation.virtual or live_link, user) for user in users
|
||||||
|
@ -1418,30 +1538,43 @@ def update_contest_mode(request):
|
||||||
|
|
||||||
ContestsSummaryData = namedtuple(
|
ContestsSummaryData = namedtuple(
|
||||||
"ContestsSummaryData",
|
"ContestsSummaryData",
|
||||||
"user points point_contests css_class",
|
"username first_name last_name points point_contests css_class",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def contests_summary_view(request, key):
|
class ContestsSummaryView(DiggPaginatorMixin, ListView):
|
||||||
try:
|
paginate_by = 50
|
||||||
contests_summary = ContestsSummary.objects.get(key=key)
|
template_name = "contest/contests_summary.html"
|
||||||
except:
|
|
||||||
raise Http404()
|
|
||||||
|
|
||||||
cache_key = "csv:" + key
|
def get(self, *args, **kwargs):
|
||||||
context = cache.get(cache_key)
|
try:
|
||||||
if context:
|
self.contests_summary = ContestsSummary.objects.get(key=kwargs["key"])
|
||||||
return render(request, "contest/contests_summary.html", context)
|
except:
|
||||||
|
raise Http404()
|
||||||
|
return super().get(*args, **kwargs)
|
||||||
|
|
||||||
scores_system = contests_summary.scores
|
def get_queryset(self):
|
||||||
contests = contests_summary.contests.all()
|
total_rank = self.contests_summary.results
|
||||||
|
return total_rank
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["contests"] = self.contests_summary.contests.all()
|
||||||
|
context["title"] = _("Contests")
|
||||||
|
context["first_page_href"] = "."
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def recalculate_contest_summary_result(contest_summary):
|
||||||
|
scores_system = contest_summary.scores
|
||||||
|
contests = contest_summary.contests.all()
|
||||||
total_points = defaultdict(int)
|
total_points = defaultdict(int)
|
||||||
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
|
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
|
||||||
user_css_class = {}
|
user_css_class = {}
|
||||||
|
|
||||||
for i in range(len(contests)):
|
for i in range(len(contests)):
|
||||||
contest = contests[i]
|
contest = contests[i]
|
||||||
users, problems = get_contest_ranking_list(request, contest)
|
users, problems = get_contest_ranking_list(None, contest)
|
||||||
for rank, user in users:
|
for rank, user in users:
|
||||||
curr_score = 0
|
curr_score = 0
|
||||||
if rank - 1 < len(scores_system):
|
if rank - 1 < len(scores_system):
|
||||||
|
@ -1452,7 +1585,9 @@ def contests_summary_view(request, key):
|
||||||
|
|
||||||
sorted_total_points = [
|
sorted_total_points = [
|
||||||
ContestsSummaryData(
|
ContestsSummaryData(
|
||||||
user=user,
|
username=user.username,
|
||||||
|
first_name=user.first_name,
|
||||||
|
last_name=user.last_name,
|
||||||
points=total_points[user],
|
points=total_points[user],
|
||||||
point_contests=result_per_contest[user],
|
point_contests=result_per_contest[user],
|
||||||
css_class=user_css_class[user],
|
css_class=user_css_class[user],
|
||||||
|
@ -1462,17 +1597,68 @@ def contests_summary_view(request, key):
|
||||||
|
|
||||||
sorted_total_points.sort(key=lambda x: x.points, reverse=True)
|
sorted_total_points.sort(key=lambda x: x.points, reverse=True)
|
||||||
total_rank = ranker(sorted_total_points)
|
total_rank = ranker(sorted_total_points)
|
||||||
|
return [(rank, item._asdict()) for rank, item in total_rank]
|
||||||
context = {
|
|
||||||
"total_rank": list(total_rank),
|
|
||||||
"title": _("Contests Summary"),
|
|
||||||
"contests": contests,
|
|
||||||
}
|
|
||||||
cache.set(cache_key, context)
|
|
||||||
|
|
||||||
return render(request, "contest/contests_summary.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver([post_save, post_delete], sender=ContestsSummary)
|
class OfficialContestList(ContestList):
|
||||||
def clear_cache(sender, instance, **kwargs):
|
official = True
|
||||||
cache.delete("csv:" + instance.key)
|
template_name = "contest/official_list.html"
|
||||||
|
|
||||||
|
def setup_contest_list(self, request):
|
||||||
|
self.contest_query = request.GET.get("contest", "")
|
||||||
|
self.org_query = []
|
||||||
|
self.hide_organization_contests = False
|
||||||
|
|
||||||
|
self.selected_categories = []
|
||||||
|
self.selected_locations = []
|
||||||
|
self.year_from = None
|
||||||
|
self.year_to = None
|
||||||
|
|
||||||
|
if "category" in request.GET:
|
||||||
|
try:
|
||||||
|
self.selected_categories = list(
|
||||||
|
map(int, request.GET.getlist("category"))
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if "location" in request.GET:
|
||||||
|
try:
|
||||||
|
self.selected_locations = list(
|
||||||
|
map(int, request.GET.getlist("location"))
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if "year_from" in request.GET:
|
||||||
|
try:
|
||||||
|
self.year_from = int(request.GET.get("year_from"))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if "year_to" in request.GET:
|
||||||
|
try:
|
||||||
|
self.year_to = int(request.GET.get("year_to"))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def extra_queryset_filters(self, queryset):
|
||||||
|
if self.selected_categories:
|
||||||
|
queryset = queryset.filter(official__category__in=self.selected_categories)
|
||||||
|
if self.selected_locations:
|
||||||
|
queryset = queryset.filter(official__location__in=self.selected_locations)
|
||||||
|
if self.year_from:
|
||||||
|
queryset = queryset.filter(official__year__gte=self.year_from)
|
||||||
|
if self.year_to:
|
||||||
|
queryset = queryset.filter(official__year__lte=self.year_to)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["page_type"] = "official"
|
||||||
|
context["is_official"] = True
|
||||||
|
context["categories"] = OfficialContestCategory.objects.all()
|
||||||
|
context["locations"] = OfficialContestLocation.objects.all()
|
||||||
|
context["selected_categories"] = self.selected_categories
|
||||||
|
context["selected_locations"] = self.selected_locations
|
||||||
|
context["year_from"] = self.year_from
|
||||||
|
context["year_to"] = self.year_to
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
|
@ -1,24 +1,68 @@
|
||||||
|
from django.utils.html import mark_safe
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from judge.models.course import Course
|
from django.views.generic import ListView, DetailView, View
|
||||||
from django.views.generic import ListView
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
|
from django.http import Http404
|
||||||
|
from django import forms
|
||||||
|
from django.forms import inlineformset_factory
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.db.models import Max, F
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
__all__ = [
|
from judge.models import Course, CourseLesson, Submission, Profile, CourseRole
|
||||||
"CourseList",
|
from judge.models.course import RoleInCourse
|
||||||
"CourseDetail",
|
from judge.widgets import HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget
|
||||||
"CourseResource",
|
from judge.utils.problems import (
|
||||||
"CourseResourceDetail",
|
user_attempted_ids,
|
||||||
"CourseStudentResults",
|
user_completed_ids,
|
||||||
"CourseEdit",
|
)
|
||||||
"CourseResourceDetailEdit",
|
|
||||||
"CourseResourceEdit",
|
|
||||||
]
|
|
||||||
|
|
||||||
course_directory_file = ""
|
|
||||||
|
|
||||||
|
|
||||||
class CourseListMixin(object):
|
def max_case_points_per_problem(profile, problems):
|
||||||
def get_queryset(self):
|
# return a dict {problem_id: {case_points, case_total}}
|
||||||
return Course.objects.filter(is_open="true").values()
|
q = (
|
||||||
|
Submission.objects.filter(user=profile, problem__in=problems)
|
||||||
|
.values("problem")
|
||||||
|
.annotate(case_points=Max("case_points"), case_total=F("case_total"))
|
||||||
|
.order_by("problem")
|
||||||
|
)
|
||||||
|
res = {}
|
||||||
|
for problem in q:
|
||||||
|
res[problem["problem"]] = problem
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_lessons_progress(profile, lessons):
|
||||||
|
res = {}
|
||||||
|
total_achieved_points = 0
|
||||||
|
total_points = 0
|
||||||
|
for lesson in lessons:
|
||||||
|
problems = list(lesson.problems.all())
|
||||||
|
if not problems:
|
||||||
|
res[lesson.id] = {"achieved_points": 0, "percentage": 0}
|
||||||
|
total_points += lesson.points
|
||||||
|
continue
|
||||||
|
problem_points = max_case_points_per_problem(profile, problems)
|
||||||
|
num_problems = len(problems)
|
||||||
|
percentage = 0
|
||||||
|
for val in problem_points.values():
|
||||||
|
score = val["case_points"] / val["case_total"]
|
||||||
|
percentage += score / num_problems
|
||||||
|
res[lesson.id] = {
|
||||||
|
"achieved_points": percentage * lesson.points,
|
||||||
|
"percentage": percentage * 100,
|
||||||
|
}
|
||||||
|
total_achieved_points += percentage * lesson.points
|
||||||
|
total_points += lesson.points
|
||||||
|
|
||||||
|
res["total"] = {
|
||||||
|
"achieved_points": total_achieved_points,
|
||||||
|
"total_points": total_points,
|
||||||
|
"percentage": total_achieved_points / total_points * 100 if total_points else 0,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class CourseList(ListView):
|
class CourseList(ListView):
|
||||||
|
@ -28,12 +72,179 @@ class CourseList(ListView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CourseList, self).get_context_data(**kwargs)
|
context = super(CourseList, self).get_context_data(**kwargs)
|
||||||
available, enrolling = [], []
|
context["courses"] = Course.get_accessible_courses(self.request.profile)
|
||||||
for course in Course.objects.filter(is_public=True).filter(is_open=True):
|
context["title"] = _("Courses")
|
||||||
if Course.is_accessible_by(course, self.request.profile):
|
context["page_type"] = "list"
|
||||||
enrolling.append(course)
|
return context
|
||||||
else:
|
|
||||||
available.append(course)
|
|
||||||
context["available"] = available
|
class CourseDetailMixin(object):
|
||||||
context["enrolling"] = enrolling
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.course = get_object_or_404(Course, slug=self.kwargs["slug"])
|
||||||
|
if not Course.is_accessible_by(self.course, self.request.profile):
|
||||||
|
raise Http404()
|
||||||
|
self.is_editable = Course.is_editable_by(self.course, self.request.profile)
|
||||||
|
return super(CourseDetailMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseDetailMixin, self).get_context_data(**kwargs)
|
||||||
|
context["course"] = self.course
|
||||||
|
context["is_editable"] = self.is_editable
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseEditableMixin(CourseDetailMixin):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
res = super(CourseEditableMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
if not self.is_editable:
|
||||||
|
raise Http404()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class CourseDetail(CourseDetailMixin, DetailView):
|
||||||
|
model = Course
|
||||||
|
template_name = "course/course.html"
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.course
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseDetail, self).get_context_data(**kwargs)
|
||||||
|
lessons = self.course.lessons.prefetch_related("problems").all()
|
||||||
|
context["title"] = self.course.name
|
||||||
|
context["page_type"] = "home"
|
||||||
|
context["lessons"] = lessons
|
||||||
|
context["lesson_progress"] = calculate_lessons_progress(
|
||||||
|
self.request.profile, lessons
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseLessonDetail(CourseDetailMixin, DetailView):
|
||||||
|
model = CourseLesson
|
||||||
|
template_name = "course/lesson.html"
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
try:
|
||||||
|
self.lesson = CourseLesson.objects.get(
|
||||||
|
course=self.course, id=self.kwargs["id"]
|
||||||
|
)
|
||||||
|
return self.lesson
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def get_profile(self):
|
||||||
|
username = self.request.GET.get("user")
|
||||||
|
if not username:
|
||||||
|
return self.request.profile
|
||||||
|
|
||||||
|
is_editable = Course.is_editable_by(self.course, self.request.profile)
|
||||||
|
if not is_editable:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
try:
|
||||||
|
profile = Profile.objects.get(user__username=username)
|
||||||
|
is_student = profile.course_roles.filter(
|
||||||
|
role=RoleInCourse.STUDENT, course=self.course
|
||||||
|
).exists()
|
||||||
|
if not is_student:
|
||||||
|
raise Http404()
|
||||||
|
return profile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseLessonDetail, self).get_context_data(**kwargs)
|
||||||
|
profile = self.get_profile()
|
||||||
|
context["title"] = self.lesson.title
|
||||||
|
context["lesson"] = self.lesson
|
||||||
|
context["completed_problem_ids"] = user_completed_ids(profile)
|
||||||
|
context["attempted_problems"] = user_attempted_ids(profile)
|
||||||
|
context["problem_points"] = max_case_points_per_problem(
|
||||||
|
profile, self.lesson.problems.all()
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseLessonForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CourseLesson
|
||||||
|
fields = ["order", "title", "points", "content", "problems"]
|
||||||
|
widgets = {
|
||||||
|
"title": forms.TextInput(),
|
||||||
|
"content": HeavyPreviewPageDownWidget(preview=reverse_lazy("blog_preview")),
|
||||||
|
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CourseLessonFormSet = inlineformset_factory(
|
||||||
|
Course, CourseLesson, form=CourseLessonForm, extra=1, can_delete=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EditCourseLessonsView(CourseEditableMixin, FormView):
|
||||||
|
template_name = "course/edit_lesson.html"
|
||||||
|
form_class = CourseLessonFormSet
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(EditCourseLessonsView, self).get_context_data(**kwargs)
|
||||||
|
if self.request.method == "POST":
|
||||||
|
context["formset"] = self.form_class(
|
||||||
|
self.request.POST, self.request.FILES, instance=self.course
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context["formset"] = self.form_class(
|
||||||
|
instance=self.course, queryset=self.course.lessons.order_by("order")
|
||||||
|
)
|
||||||
|
context["title"] = _("Edit lessons for %(course_name)s") % {
|
||||||
|
"course_name": self.course.name
|
||||||
|
}
|
||||||
|
context["content_title"] = mark_safe(
|
||||||
|
_("Edit lessons for <a href='%(url)s'>%(course_name)s</a>")
|
||||||
|
% {
|
||||||
|
"course_name": self.course.name,
|
||||||
|
"url": self.course.get_absolute_url(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context["page_type"] = "edit_lesson"
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
formset = self.form_class(request.POST, instance=self.course)
|
||||||
|
if formset.is_valid():
|
||||||
|
formset.save()
|
||||||
|
return self.form_valid(formset)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(formset)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.path
|
||||||
|
|
||||||
|
|
||||||
|
class CourseStudentResults(CourseEditableMixin, DetailView):
|
||||||
|
model = Course
|
||||||
|
template_name = "course/grades.html"
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.course
|
||||||
|
|
||||||
|
def get_grades(self):
|
||||||
|
students = self.course.get_students()
|
||||||
|
students.sort(key=lambda u: u.username.lower())
|
||||||
|
lessons = self.course.lessons.prefetch_related("problems").all()
|
||||||
|
grades = {s: calculate_lessons_progress(s, lessons) for s in students}
|
||||||
|
return grades
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseStudentResults, self).get_context_data(**kwargs)
|
||||||
|
context["title"] = mark_safe(
|
||||||
|
_("Grades in <a href='%(url)s'>%(course_name)s</a>")
|
||||||
|
% {
|
||||||
|
"course_name": self.course.name,
|
||||||
|
"url": self.course.get_absolute_url(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context["page_type"] = "grades"
|
||||||
|
context["grades"] = self.get_grades()
|
||||||
return context
|
return context
|
||||||
|
|
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.views.generic import ListView
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
from judge.models import Profile, Notification, NotificationProfile
|
from judge.models import Profile, Notification, NotificationProfile
|
||||||
from judge.models.notification import unseen_notifications_count
|
from judge.models.notification import unseen_notifications_count
|
||||||
|
from judge.utils.infinite_paginator import InfinitePaginationMixin
|
||||||
|
|
||||||
__all__ = ["NotificationList"]
|
__all__ = ["NotificationList"]
|
||||||
|
|
||||||
|
|
||||||
class NotificationList(ListView):
|
class NotificationList(InfinitePaginationMixin, ListView):
|
||||||
model = Notification
|
model = Notification
|
||||||
context_object_name = "notifications"
|
context_object_name = "notifications"
|
||||||
template_name = "notification/list.html"
|
template_name = "notification/list.html"
|
||||||
|
paginate_by = 50
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.unseen_cnt = unseen_notifications_count(self.request.profile)
|
self.unseen_cnt = unseen_notifications_count(self.request.profile)
|
||||||
|
|
||||||
self.queryset = Notification.objects.filter(
|
self.queryset = Notification.objects.filter(
|
||||||
owner=self.request.profile
|
owner=self.request.profile
|
||||||
).order_by("-id")[:100]
|
).order_by("-id")
|
||||||
|
|
||||||
return self.queryset
|
return self.queryset
|
||||||
|
|
||||||
|
@ -27,11 +30,13 @@ class NotificationList(ListView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["unseen_count"] = self.unseen_cnt
|
context["unseen_count"] = self.unseen_cnt
|
||||||
context["title"] = _("Notifications (%d unseen)") % context["unseen_count"]
|
context["title"] = _("Notifications (%d unseen)") % context["unseen_count"]
|
||||||
context["has_notifications"] = self.queryset.exists()
|
context["first_page_href"] = "."
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
ret = super().get(request, *args, **kwargs)
|
ret = super().get(request, *args, **kwargs)
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
raise Http404()
|
||||||
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
|
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
|
||||||
unseen_notifications_count.dirty(self.request.profile)
|
unseen_notifications_count.dirty(self.request.profile)
|
||||||
return ret
|
return ret
|
||||||
|
|
|
@ -71,7 +71,7 @@ from judge.utils.views import (
|
||||||
from judge.utils.problems import user_attempted_ids, user_completed_ids
|
from judge.utils.problems import user_attempted_ids, user_completed_ids
|
||||||
from judge.views.problem import ProblemList
|
from judge.views.problem import ProblemList
|
||||||
from judge.views.contests import ContestList
|
from judge.views.contests import ContestList
|
||||||
from judge.views.submission import AllSubmissions, SubmissionsListBase
|
from judge.views.submission import SubmissionsListBase
|
||||||
from judge.views.feed import FeedView
|
from judge.views.feed import FeedView
|
||||||
from judge.tasks import rescore_contest
|
from judge.tasks import rescore_contest
|
||||||
|
|
||||||
|
@ -104,15 +104,15 @@ class OrganizationBase(object):
|
||||||
def is_member(self, org=None):
|
def is_member(self, org=None):
|
||||||
if org is None:
|
if org is None:
|
||||||
org = self.object
|
org = self.object
|
||||||
return (
|
if self.request.profile:
|
||||||
self.request.profile in org if self.request.user.is_authenticated else False
|
return org.is_member(self.request.profile)
|
||||||
)
|
return False
|
||||||
|
|
||||||
def is_admin(self, org=None):
|
def is_admin(self, org=None):
|
||||||
if org is None:
|
if org is None:
|
||||||
org = self.object
|
org = self.object
|
||||||
if self.request.profile:
|
if self.request.profile:
|
||||||
return org.admins.filter(id=self.request.profile.id).exists()
|
return org.is_admin(self.request.profile)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_access(self, org):
|
def can_access(self, org):
|
||||||
|
@ -131,6 +131,13 @@ class OrganizationMixin(OrganizationBase):
|
||||||
context["can_edit"] = self.can_edit_organization(self.organization)
|
context["can_edit"] = self.can_edit_organization(self.organization)
|
||||||
context["organization"] = self.organization
|
context["organization"] = self.organization
|
||||||
context["logo_override_image"] = self.organization.logo_override_image
|
context["logo_override_image"] = self.organization.logo_override_image
|
||||||
|
context["organization_subdomain"] = (
|
||||||
|
("http" if settings.DMOJ_SSL == 0 else "https")
|
||||||
|
+ "://"
|
||||||
|
+ self.organization.slug
|
||||||
|
+ "."
|
||||||
|
+ get_current_site(self.request).domain
|
||||||
|
)
|
||||||
if "organizations" in context:
|
if "organizations" in context:
|
||||||
context.pop("organizations")
|
context.pop("organizations")
|
||||||
return context
|
return context
|
||||||
|
@ -215,41 +222,103 @@ class OrganizationHomeView(OrganizationMixin):
|
||||||
organizations=self.organization,
|
organizations=self.organization,
|
||||||
authors=self.request.profile,
|
authors=self.request.profile,
|
||||||
).count()
|
).count()
|
||||||
context["top_rated"] = self.organization.members.filter(
|
context["top_rated"] = (
|
||||||
is_unlisted=False
|
self.organization.members.filter(is_unlisted=False)
|
||||||
).order_by("-rating")[:10]
|
.order_by("-rating")
|
||||||
context["top_scorer"] = self.organization.members.filter(
|
.only("id", "rating")[:10]
|
||||||
is_unlisted=False
|
)
|
||||||
).order_by("-performance_points")[:10]
|
context["top_scorer"] = (
|
||||||
|
self.organization.members.filter(is_unlisted=False)
|
||||||
|
.order_by("-performance_points")
|
||||||
|
.only("id", "performance_points")[:10]
|
||||||
|
)
|
||||||
|
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
|
||||||
|
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class OrganizationList(TitleMixin, ListView, OrganizationBase):
|
class OrganizationList(
|
||||||
|
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView, OrganizationBase
|
||||||
|
):
|
||||||
model = Organization
|
model = Organization
|
||||||
context_object_name = "organizations"
|
context_object_name = "organizations"
|
||||||
template_name = "organization/list.html"
|
template_name = "organization/list.html"
|
||||||
title = gettext_lazy("Groups")
|
title = gettext_lazy("Groups")
|
||||||
|
paginate_by = 12
|
||||||
|
all_sorts = frozenset(("name", "member_count"))
|
||||||
|
default_desc = frozenset(("name", "member_count"))
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_default_sort_order(self, request):
|
||||||
return (
|
return "-member_count"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
default_tab = "mine"
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
default_tab = "public"
|
||||||
|
self.current_tab = self.request.GET.get("tab", default_tab)
|
||||||
|
self.organization_query = request.GET.get("organization", "")
|
||||||
|
|
||||||
|
return super(OrganizationList, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def _get_queryset(self):
|
||||||
|
queryset = (
|
||||||
super(OrganizationList, self)
|
super(OrganizationList, self)
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.annotate(member_count=Count("member"))
|
.annotate(member_count=Count("member"))
|
||||||
|
.defer("about")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.organization_query:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(slug__icontains=self.organization_query)
|
||||||
|
| Q(name__icontains=self.organization_query)
|
||||||
|
| Q(short_name__icontains=self.organization_query)
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
organization_list = self._get_queryset()
|
||||||
|
|
||||||
|
my_organizations = []
|
||||||
|
if self.request.profile:
|
||||||
|
my_organizations = organization_list.filter(
|
||||||
|
id__in=self.request.profile.organizations.values("id")
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.current_tab == "public":
|
||||||
|
queryset = organization_list.exclude(id__in=my_organizations).filter(
|
||||||
|
is_open=True
|
||||||
|
)
|
||||||
|
elif self.current_tab == "private":
|
||||||
|
queryset = organization_list.exclude(id__in=my_organizations).filter(
|
||||||
|
is_open=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
queryset = my_organizations
|
||||||
|
|
||||||
|
if queryset:
|
||||||
|
queryset = queryset.order_by(self.order)
|
||||||
|
return queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(OrganizationList, self).get_context_data(**kwargs)
|
context = super(OrganizationList, self).get_context_data(**kwargs)
|
||||||
context["my_organizations"] = []
|
|
||||||
context["page_type"] = "organizations"
|
context["first_page_href"] = "."
|
||||||
if self.request.profile:
|
context["current_tab"] = self.current_tab
|
||||||
context["my_organizations"] = context["organizations"].filter(
|
context["page_type"] = self.current_tab
|
||||||
id__in=self.request.profile.organizations.values("id")
|
context["organization_query"] = self.organization_query
|
||||||
)
|
context["selected_order"] = self.request.GET.get("order")
|
||||||
other_organizations = context["organizations"].exclude(
|
context["all_sort_options"] = [
|
||||||
id__in=context["my_organizations"]
|
("name", _("Name (asc.)")),
|
||||||
)
|
("-name", _("Name (desc.)")),
|
||||||
context["open_organizations"] = other_organizations.filter(is_open=True)
|
("member_count", _("Member count (asc.)")),
|
||||||
context["private_organizations"] = other_organizations.filter(is_open=False)
|
("-member_count", _("Member count (desc.)")),
|
||||||
|
]
|
||||||
|
|
||||||
|
context.update(self.get_sort_context())
|
||||||
|
context.update(self.get_sort_paginate_context())
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -274,14 +343,6 @@ class OrganizationHome(OrganizationHomeView, FeedView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(OrganizationHome, self).get_context_data(**kwargs)
|
context = super(OrganizationHome, self).get_context_data(**kwargs)
|
||||||
context["title"] = self.organization.name
|
context["title"] = self.organization.name
|
||||||
http = "http" if settings.DMOJ_SSL == 0 else "https"
|
|
||||||
context["organization_subdomain"] = (
|
|
||||||
http
|
|
||||||
+ "://"
|
|
||||||
+ self.organization.slug
|
|
||||||
+ "."
|
|
||||||
+ get_current_site(self.request).domain
|
|
||||||
)
|
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
visible_contests = (
|
visible_contests = (
|
||||||
|
@ -407,6 +468,7 @@ class OrganizationContests(
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.org_query = [self.organization_id]
|
self.org_query = [self.organization_id]
|
||||||
|
self.hide_organization_contests = False
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
def set_editable_contest(self, contest):
|
def set_editable_contest(self, contest):
|
||||||
|
@ -417,21 +479,20 @@ class OrganizationContests(
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(OrganizationContests, self).get_context_data(**kwargs)
|
context = super(OrganizationContests, self).get_context_data(**kwargs)
|
||||||
context["page_type"] = "contests"
|
context["page_type"] = "contests"
|
||||||
context["hide_contest_orgs"] = True
|
|
||||||
context.pop("organizations")
|
context.pop("organizations")
|
||||||
context["create_url"] = reverse(
|
|
||||||
"organization_contest_add",
|
|
||||||
args=[self.organization.id, self.organization.slug],
|
|
||||||
)
|
|
||||||
|
|
||||||
for participation in context["active_participations"]:
|
if self.can_edit_organization(self.organization):
|
||||||
self.set_editable_contest(participation.contest)
|
context["create_url"] = reverse(
|
||||||
for contest in context["past_contests"]:
|
"organization_contest_add",
|
||||||
self.set_editable_contest(contest)
|
args=[self.organization.id, self.organization.slug],
|
||||||
for contest in context["current_contests"]:
|
)
|
||||||
self.set_editable_contest(contest)
|
|
||||||
for contest in context["future_contests"]:
|
if self.current_tab == "active":
|
||||||
self.set_editable_contest(contest)
|
for participation in context["contests"]:
|
||||||
|
self.set_editable_contest(participation.contest)
|
||||||
|
else:
|
||||||
|
for contest in context["contests"]:
|
||||||
|
self.set_editable_contest(contest)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -471,6 +532,9 @@ class OrganizationSubmissions(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_title(self):
|
||||||
|
return _("Submissions in") + f" {self.organization}"
|
||||||
|
|
||||||
|
|
||||||
class OrganizationMembershipChange(
|
class OrganizationMembershipChange(
|
||||||
LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
|
LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
|
||||||
|
@ -516,6 +580,7 @@ class JoinOrganization(OrganizationMembershipChange):
|
||||||
profile.organizations.add(org)
|
profile.organizations.add(org)
|
||||||
profile.save()
|
profile.save()
|
||||||
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
|
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
|
||||||
|
Organization.is_member.dirty(org, profile)
|
||||||
|
|
||||||
|
|
||||||
class LeaveOrganization(OrganizationMembershipChange):
|
class LeaveOrganization(OrganizationMembershipChange):
|
||||||
|
@ -528,6 +593,7 @@ class LeaveOrganization(OrganizationMembershipChange):
|
||||||
)
|
)
|
||||||
profile.organizations.remove(org)
|
profile.organizations.remove(org)
|
||||||
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
|
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
|
||||||
|
Organization.is_member.dirty(org, profile)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRequestForm(Form):
|
class OrganizationRequestForm(Form):
|
||||||
|
@ -737,7 +803,7 @@ class AddOrganizationMember(
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
new_users = form.cleaned_data["new_users"]
|
new_users = form.cleaned_data["new_users"]
|
||||||
self.object.members.add(*new_users)
|
self.object.members.add(*new_users)
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
revisions.set_comment(_("Added members from site"))
|
revisions.set_comment(_("Added members from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
return super(AddOrganizationMember, self).form_valid(form)
|
return super(AddOrganizationMember, self).form_valid(form)
|
||||||
|
@ -804,7 +870,7 @@ class EditOrganization(
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
revisions.set_comment(_("Edited from site"))
|
revisions.set_comment(_("Edited from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
return super(EditOrganization, self).form_valid(form)
|
return super(EditOrganization, self).form_valid(form)
|
||||||
|
@ -836,7 +902,7 @@ class AddOrganization(LoginRequiredMixin, TitleMixin, CreateView):
|
||||||
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
|
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
revisions.set_comment(_("Added from site"))
|
revisions.set_comment(_("Added from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
res = super(AddOrganization, self).form_valid(form)
|
res = super(AddOrganization, self).form_valid(form)
|
||||||
|
@ -861,7 +927,7 @@ class AddOrganizationContest(
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
revisions.set_comment(_("Added from site"))
|
revisions.set_comment(_("Added from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
|
|
||||||
|
@ -954,7 +1020,7 @@ class EditOrganizationContest(
|
||||||
return self.contest
|
return self.contest
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
revisions.set_comment(_("Edited from site"))
|
revisions.set_comment(_("Edited from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
res = super(EditOrganizationContest, self).form_valid(form)
|
res = super(EditOrganizationContest, self).form_valid(form)
|
||||||
|
@ -974,6 +1040,18 @@ class EditOrganizationContest(
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
transaction.on_commit(rescore_contest.s(self.object.key).delay)
|
transaction.on_commit(rescore_contest.s(self.object.key).delay)
|
||||||
|
|
||||||
|
if any(
|
||||||
|
f in form.changed_data
|
||||||
|
for f in (
|
||||||
|
"authors",
|
||||||
|
"curators",
|
||||||
|
"testers",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
Contest._author_ids.dirty(self.object)
|
||||||
|
Contest._curator_ids.dirty(self.object)
|
||||||
|
Contest._tester_ids.dirty(self.object)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_problem_formset(self, post=False):
|
def get_problem_formset(self, post=False):
|
||||||
|
@ -1015,7 +1093,7 @@ class AddOrganizationBlog(
|
||||||
return _("Add blog for %s") % self.organization.name
|
return _("Add blog for %s") % self.organization.name
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
res = super(AddOrganizationBlog, self).form_valid(form)
|
res = super(AddOrganizationBlog, self).form_valid(form)
|
||||||
self.object.is_organization_private = True
|
self.object.is_organization_private = True
|
||||||
self.object.authors.add(self.request.profile)
|
self.object.authors.add(self.request.profile)
|
||||||
|
@ -1038,6 +1116,11 @@ class AddOrganizationBlog(
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse(
|
||||||
|
"organization_home", args=[self.organization.id, self.organization.slug]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EditOrganizationBlog(
|
class EditOrganizationBlog(
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
|
@ -1061,7 +1144,7 @@ class EditOrganizationBlog(
|
||||||
if self.organization not in self.blog.organizations.all():
|
if self.organization not in self.blog.organizations.all():
|
||||||
raise Exception("This blog does not belong to this organization")
|
raise Exception("This blog does not belong to this organization")
|
||||||
if (
|
if (
|
||||||
self.request.profile not in self.blog.authors.all()
|
self.request.profile.id not in self.blog.get_authors()
|
||||||
and not self.can_edit_organization(self.organization)
|
and not self.can_edit_organization(self.organization)
|
||||||
):
|
):
|
||||||
raise Exception("Not allowed to edit this blog")
|
raise Exception("Not allowed to edit this blog")
|
||||||
|
@ -1115,13 +1198,18 @@ class EditOrganizationBlog(
|
||||||
make_notification(posible_users, action, html, self.request.profile)
|
make_notification(posible_users, action, html, self.request.profile)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with revisions.create_revision():
|
||||||
res = super(EditOrganizationBlog, self).form_valid(form)
|
res = super(EditOrganizationBlog, self).form_valid(form)
|
||||||
revisions.set_comment(_("Edited from site"))
|
revisions.set_comment(_("Edited from site"))
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
self.create_notification("Edit blog")
|
self.create_notification("Edit blog")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse(
|
||||||
|
"organization_home", args=[self.organization.id, self.organization.slug]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PendingBlogs(
|
class PendingBlogs(
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
|
|
|
@ -8,13 +8,13 @@ from django.http import (
|
||||||
HttpResponseForbidden,
|
HttpResponseForbidden,
|
||||||
)
|
)
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from judge.models.pagevote import PageVote, PageVoteVoter
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
from judge.dblock import LockModel
|
|
||||||
from django.views.generic import View, ListView
|
from django.views.generic import View, ListView
|
||||||
|
from django_ratelimit.decorators import ratelimit
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from judge.models.pagevote import PageVote, PageVoteVoter, dirty_pagevote
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"upvote_page",
|
"upvote_page",
|
||||||
|
@ -24,6 +24,7 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ratelimit(key="user", rate=settings.RL_VOTE)
|
||||||
@login_required
|
@login_required
|
||||||
def vote_page(request, delta):
|
def vote_page(request, delta):
|
||||||
if abs(delta) != 1:
|
if abs(delta) != 1:
|
||||||
|
@ -52,35 +53,33 @@ def vote_page(request, delta):
|
||||||
pagevote_id = int(request.POST["id"])
|
pagevote_id = int(request.POST["id"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
else:
|
|
||||||
if not PageVote.objects.filter(id=pagevote_id).exists():
|
try:
|
||||||
raise Http404()
|
pagevote = PageVote.objects.get(id=pagevote_id)
|
||||||
|
except PageVote.DoesNotExist:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
vote = PageVoteVoter()
|
vote = PageVoteVoter()
|
||||||
vote.pagevote_id = pagevote_id
|
vote.pagevote_id = pagevote_id
|
||||||
vote.voter = request.profile
|
vote.voter = request.profile
|
||||||
vote.score = delta
|
vote.score = delta
|
||||||
|
|
||||||
while True:
|
try:
|
||||||
|
vote.save()
|
||||||
|
except IntegrityError:
|
||||||
try:
|
try:
|
||||||
vote.save()
|
vote = PageVoteVoter.objects.get(
|
||||||
except IntegrityError:
|
pagevote_id=pagevote_id, voter=request.profile
|
||||||
with LockModel(write=(PageVoteVoter,)):
|
|
||||||
try:
|
|
||||||
vote = PageVoteVoter.objects.get(
|
|
||||||
pagevote_id=pagevote_id, voter=request.profile
|
|
||||||
)
|
|
||||||
except PageVoteVoter.DoesNotExist:
|
|
||||||
# We must continue racing in case this is exploited to manipulate votes.
|
|
||||||
continue
|
|
||||||
vote.delete()
|
|
||||||
PageVote.objects.filter(id=pagevote_id).update(
|
|
||||||
score=F("score") - vote.score
|
|
||||||
)
|
)
|
||||||
else:
|
except PageVoteVoter.DoesNotExist:
|
||||||
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
|
raise Http404()
|
||||||
break
|
vote.delete()
|
||||||
_dirty_vote_score(pagevote_id, request.profile)
|
PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
|
||||||
|
else:
|
||||||
|
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
|
||||||
|
|
||||||
|
dirty_pagevote(pagevote, request.profile)
|
||||||
|
|
||||||
return HttpResponse("success", content_type="text/plain")
|
return HttpResponse("success", content_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,8 +103,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
||||||
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
|
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
|
||||||
context["pagevote"] = self.object.get_or_create_pagevote()
|
context["pagevote"] = self.object.get_or_create_pagevote()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def _dirty_vote_score(pagevote_id, profile):
|
|
||||||
pv = PageVote(id=pagevote_id)
|
|
||||||
pv.vote_score.dirty(pv, profile)
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import timedelta, datetime
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from random import randrange
|
from random import randrange
|
||||||
import random
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
@ -24,6 +22,7 @@ from django.db.models import (
|
||||||
Q,
|
Q,
|
||||||
When,
|
When,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
|
Sum,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.db.utils import ProgrammingError
|
from django.db.utils import ProgrammingError
|
||||||
|
@ -46,7 +45,7 @@ from django.views.generic import ListView, View
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
from judge.comments import CommentedDetailView
|
from judge.views.comment import CommentedDetailView
|
||||||
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
|
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
ContestProblem,
|
ContestProblem,
|
||||||
|
@ -66,6 +65,7 @@ from judge.models import (
|
||||||
Organization,
|
Organization,
|
||||||
Profile,
|
Profile,
|
||||||
LanguageTemplate,
|
LanguageTemplate,
|
||||||
|
Contest,
|
||||||
)
|
)
|
||||||
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
|
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
|
||||||
from judge.utils.diggpaginator import DiggPaginator
|
from judge.utils.diggpaginator import DiggPaginator
|
||||||
|
@ -77,6 +77,8 @@ from judge.utils.problems import (
|
||||||
user_attempted_ids,
|
user_attempted_ids,
|
||||||
user_completed_ids,
|
user_completed_ids,
|
||||||
get_related_problems,
|
get_related_problems,
|
||||||
|
get_user_recommended_problems,
|
||||||
|
RecommendationType,
|
||||||
)
|
)
|
||||||
from judge.utils.strings import safe_float_or_none, safe_int_or_none
|
from judge.utils.strings import safe_float_or_none, safe_int_or_none
|
||||||
from judge.utils.tickets import own_ticket_filter
|
from judge.utils.tickets import own_ticket_filter
|
||||||
|
@ -351,7 +353,7 @@ class ProblemDetail(
|
||||||
else:
|
else:
|
||||||
context["fileio_input"] = None
|
context["fileio_input"] = None
|
||||||
context["fileio_output"] = None
|
context["fileio_output"] = None
|
||||||
if not self.in_contest:
|
if not self.in_contest and settings.ML_OUTPUT_PATH:
|
||||||
context["related_problems"] = get_related_problems(
|
context["related_problems"] = get_related_problems(
|
||||||
self.profile, self.object
|
self.profile, self.object
|
||||||
)
|
)
|
||||||
|
@ -399,16 +401,13 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
|
||||||
if trans is None
|
if trans is None
|
||||||
else trans.description,
|
else trans.description,
|
||||||
"url": request.build_absolute_uri(),
|
"url": request.build_absolute_uri(),
|
||||||
"math_engine": maker.math_engine,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.replace('"//', '"https://')
|
.replace('"//', '"https://')
|
||||||
.replace("'//", "'https://")
|
.replace("'//", "'https://")
|
||||||
)
|
)
|
||||||
maker.title = problem_name
|
maker.title = problem_name
|
||||||
assets = ["style.css", "pygment-github.css"]
|
assets = ["style.css"]
|
||||||
if maker.math_engine == "jax":
|
|
||||||
assets.append("mathjax3_config.js")
|
|
||||||
for file in assets:
|
for file in assets:
|
||||||
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
||||||
maker.make()
|
maker.make()
|
||||||
|
@ -590,7 +589,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
||||||
i
|
i
|
||||||
for i in query
|
for i in query
|
||||||
if i in self.profile.organizations.values_list("id", flat=True)
|
if i in self.profile.organizations.values_list("id", flat=True)
|
||||||
]
|
][:3]
|
||||||
|
|
||||||
def get_normal_queryset(self):
|
def get_normal_queryset(self):
|
||||||
queryset = Problem.get_visible_problems(self.request.user)
|
queryset = Problem.get_visible_problems(self.request.user)
|
||||||
|
@ -602,9 +601,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
||||||
self.org_query = [self.request.organization.id]
|
self.org_query = [self.request.organization.id]
|
||||||
if self.org_query:
|
if self.org_query:
|
||||||
self.org_query = self.get_org_query(self.org_query)
|
self.org_query = self.get_org_query(self.org_query)
|
||||||
|
contest_problems = (
|
||||||
|
Contest.objects.filter(organizations__in=self.org_query)
|
||||||
|
.select_related("problems")
|
||||||
|
.values_list("contest_problems__problem__id")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(organizations__in=self.org_query)
|
Q(organizations__in=self.org_query) | Q(id__in=contest_problems)
|
||||||
| Q(contests__contest__organizations__in=self.org_query)
|
|
||||||
)
|
)
|
||||||
if self.author_query:
|
if self.author_query:
|
||||||
queryset = queryset.filter(authors__in=self.author_query)
|
queryset = queryset.filter(authors__in=self.author_query)
|
||||||
|
@ -641,6 +645,16 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
||||||
queryset = queryset.filter(points__gte=self.point_start)
|
queryset = queryset.filter(points__gte=self.point_start)
|
||||||
if self.point_end is not None:
|
if self.point_end is not None:
|
||||||
queryset = queryset.filter(points__lte=self.point_end)
|
queryset = queryset.filter(points__lte=self.point_end)
|
||||||
|
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
has_public_editorial=Sum(
|
||||||
|
Case(
|
||||||
|
When(solution__is_public=True, then=1),
|
||||||
|
default=0,
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
return queryset.distinct()
|
return queryset.distinct()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -664,12 +678,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
||||||
|
|
||||||
if self.request.profile:
|
if self.request.profile:
|
||||||
context["organizations"] = self.request.profile.organizations.all()
|
context["organizations"] = self.request.profile.organizations.all()
|
||||||
all_authors_ids = Problem.objects.values_list("authors", flat=True)
|
|
||||||
context["all_authors"] = (
|
|
||||||
Profile.objects.filter(id__in=all_authors_ids)
|
|
||||||
.select_related("user")
|
|
||||||
.values("id", "user__username")
|
|
||||||
)
|
|
||||||
context["category"] = self.category
|
context["category"] = self.category
|
||||||
context["categories"] = ProblemGroup.objects.all()
|
context["categories"] = ProblemGroup.objects.all()
|
||||||
if self.show_types:
|
if self.show_types:
|
||||||
|
@ -677,7 +685,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
||||||
context["problem_types"] = ProblemType.objects.all()
|
context["problem_types"] = ProblemType.objects.all()
|
||||||
context["has_fts"] = settings.ENABLE_FTS
|
context["has_fts"] = settings.ENABLE_FTS
|
||||||
context["org_query"] = self.org_query
|
context["org_query"] = self.org_query
|
||||||
context["author_query"] = self.author_query
|
context["author_query"] = Profile.objects.filter(id__in=self.author_query)
|
||||||
context["search_query"] = self.search_query
|
context["search_query"] = self.search_query
|
||||||
context["completed_problem_ids"] = self.get_completed_problems()
|
context["completed_problem_ids"] = self.get_completed_problems()
|
||||||
context["attempted_problems"] = self.get_attempted_problems()
|
context["attempted_problems"] = self.get_attempted_problems()
|
||||||
|
@ -829,29 +837,39 @@ class ProblemFeed(ProblemList, FeedView):
|
||||||
model = Problem
|
model = Problem
|
||||||
context_object_name = "problems"
|
context_object_name = "problems"
|
||||||
template_name = "problem/feed.html"
|
template_name = "problem/feed.html"
|
||||||
feed_content_template_name = "problem/feed/problems.html"
|
feed_content_template_name = "problem/feed/items.html"
|
||||||
paginate_by = 4
|
paginate_by = 4
|
||||||
title = _("Problem feed")
|
title = _("Problem feed")
|
||||||
feed_type = None
|
feed_type = None
|
||||||
|
|
||||||
# arr = [[], [], ..]
|
def get_recommended_problem_ids(self, queryset):
|
||||||
def merge_recommendation(self, arr):
|
user_id = self.request.profile.id
|
||||||
seed = datetime.now().strftime("%d%m%Y")
|
problem_ids = queryset.values_list("id", flat=True)
|
||||||
merged_array = []
|
rec_types = [
|
||||||
for a in arr:
|
RecommendationType.CF_DOT,
|
||||||
merged_array += a
|
RecommendationType.CF_COSINE,
|
||||||
random.Random(seed).shuffle(merged_array)
|
RecommendationType.CF_TIME_DOT,
|
||||||
|
RecommendationType.CF_TIME_COSINE,
|
||||||
|
RecommendationType.HOT_PROBLEM,
|
||||||
|
]
|
||||||
|
limits = [100, 100, 100, 100, 20]
|
||||||
|
shuffle = True
|
||||||
|
|
||||||
res = []
|
allow_debug_type = (
|
||||||
used_pid = set()
|
self.request.user.is_impersonate or self.request.user.is_superuser
|
||||||
|
)
|
||||||
|
if allow_debug_type and "debug_type" in self.request.GET:
|
||||||
|
try:
|
||||||
|
debug_type = int(self.request.GET.get("debug_type"))
|
||||||
|
except ValueError:
|
||||||
|
raise Http404()
|
||||||
|
rec_types = [debug_type]
|
||||||
|
limits = [100]
|
||||||
|
shuffle = False
|
||||||
|
|
||||||
for obj in merged_array:
|
return get_user_recommended_problems(
|
||||||
if type(obj) == tuple:
|
user_id, problem_ids, rec_types, limits, shuffle
|
||||||
obj = obj[1]
|
)
|
||||||
if obj not in used_pid:
|
|
||||||
res.append(obj)
|
|
||||||
used_pid.add(obj)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.feed_type == "volunteer":
|
if self.feed_type == "volunteer":
|
||||||
|
@ -885,40 +903,8 @@ class ProblemFeed(ProblemList, FeedView):
|
||||||
if not settings.ML_OUTPUT_PATH or not user:
|
if not settings.ML_OUTPUT_PATH or not user:
|
||||||
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
|
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
|
||||||
|
|
||||||
cf_model = CollabFilter("collab_filter")
|
q = self.get_recommended_problem_ids(queryset)
|
||||||
cf_time_model = CollabFilter("collab_filter_time")
|
|
||||||
|
|
||||||
queryset = queryset.values_list("id", flat=True)
|
|
||||||
hot_problems_recommendations = [
|
|
||||||
problem.id
|
|
||||||
for problem in hot_problems(timedelta(days=7), 20)
|
|
||||||
if problem.id in set(queryset)
|
|
||||||
]
|
|
||||||
|
|
||||||
q = self.merge_recommendation(
|
|
||||||
[
|
|
||||||
cf_model.user_recommendations(user, queryset, cf_model.DOT, 100),
|
|
||||||
cf_model.user_recommendations(
|
|
||||||
user,
|
|
||||||
queryset,
|
|
||||||
cf_model.COSINE,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
cf_time_model.user_recommendations(
|
|
||||||
user,
|
|
||||||
queryset,
|
|
||||||
cf_time_model.COSINE,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
cf_time_model.user_recommendations(
|
|
||||||
user,
|
|
||||||
queryset,
|
|
||||||
cf_time_model.DOT,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
hot_problems_recommendations,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
queryset = Problem.objects.filter(id__in=q)
|
queryset = Problem.objects.filter(id__in=q)
|
||||||
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
|
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
|
||||||
|
|
||||||
|
@ -974,6 +960,12 @@ class LanguageTemplateAjax(View):
|
||||||
class RandomProblem(ProblemList):
|
class RandomProblem(ProblemList):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.setup_problem_list(request)
|
self.setup_problem_list(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
except ProgrammingError as e:
|
||||||
|
return generic_message(request, "FTS syntax error", e.args[1], status=400)
|
||||||
|
|
||||||
if self.in_contest:
|
if self.in_contest:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
|
@ -994,6 +986,15 @@ class RandomProblem(ProblemList):
|
||||||
user_logger = logging.getLogger("judge.user")
|
user_logger = logging.getLogger("judge.user")
|
||||||
|
|
||||||
|
|
||||||
|
def last_nth_submitted_date_in_contest(profile, contest, n):
|
||||||
|
submissions = Submission.objects.filter(
|
||||||
|
user=profile, contest_object=contest
|
||||||
|
).order_by("-id")[:n]
|
||||||
|
if submissions.count() >= n:
|
||||||
|
return submissions[n - 1].date
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def problem_submit(request, problem, submission=None):
|
def problem_submit(request, problem, submission=None):
|
||||||
if (
|
if (
|
||||||
|
@ -1042,7 +1043,7 @@ def problem_submit(request, problem, submission=None):
|
||||||
>= settings.DMOJ_SUBMISSION_LIMIT
|
>= settings.DMOJ_SUBMISSION_LIMIT
|
||||||
):
|
):
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
"<h1>You submitted too many submissions.</h1>", status=429
|
_("<h1>You have submitted too many submissions.</h1>"), status=429
|
||||||
)
|
)
|
||||||
if not problem.allowed_languages.filter(
|
if not problem.allowed_languages.filter(
|
||||||
id=form.cleaned_data["language"].id
|
id=form.cleaned_data["language"].id
|
||||||
|
@ -1063,7 +1064,22 @@ def problem_submit(request, problem, submission=None):
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if profile.current_contest is not None:
|
if profile.current_contest is not None:
|
||||||
|
contest = profile.current_contest.contest
|
||||||
contest_id = profile.current_contest.contest_id
|
contest_id = profile.current_contest.contest_id
|
||||||
|
rate_limit = contest.rate_limit
|
||||||
|
|
||||||
|
if rate_limit:
|
||||||
|
t = last_nth_submitted_date_in_contest(
|
||||||
|
profile, contest, rate_limit
|
||||||
|
)
|
||||||
|
if t is not None and timezone.now() - t < timezone.timedelta(
|
||||||
|
minutes=1
|
||||||
|
):
|
||||||
|
return HttpResponse(
|
||||||
|
_("<h1>You have submitted too many submissions.</h1>"),
|
||||||
|
status=429,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
contest_problem = problem.contests.get(contest_id=contest_id)
|
contest_problem = problem.contests.get(contest_id=contest_id)
|
||||||
except ContestProblem.DoesNotExist:
|
except ContestProblem.DoesNotExist:
|
||||||
|
@ -1143,11 +1159,11 @@ def problem_submit(request, problem, submission=None):
|
||||||
default_lang = request.profile.language
|
default_lang = request.profile.language
|
||||||
|
|
||||||
submission_limit = submissions_left = None
|
submission_limit = submissions_left = None
|
||||||
|
next_valid_submit_time = None
|
||||||
if profile.current_contest is not None:
|
if profile.current_contest is not None:
|
||||||
|
contest = profile.current_contest.contest
|
||||||
try:
|
try:
|
||||||
submission_limit = problem.contests.get(
|
submission_limit = problem.contests.get(contest=contest).max_submissions
|
||||||
contest=profile.current_contest.contest
|
|
||||||
).max_submissions
|
|
||||||
except ContestProblem.DoesNotExist:
|
except ContestProblem.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -1155,6 +1171,12 @@ def problem_submit(request, problem, submission=None):
|
||||||
submissions_left = submission_limit - get_contest_submission_count(
|
submissions_left = submission_limit - get_contest_submission_count(
|
||||||
problem, profile, profile.current_contest.virtual
|
problem, profile, profile.current_contest.virtual
|
||||||
)
|
)
|
||||||
|
if contest.rate_limit:
|
||||||
|
t = last_nth_submitted_date_in_contest(profile, contest, contest.rate_limit)
|
||||||
|
if t is not None:
|
||||||
|
next_valid_submit_time = t + timezone.timedelta(minutes=1)
|
||||||
|
next_valid_submit_time = next_valid_submit_time.isoformat()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"problem/submit.html",
|
"problem/submit.html",
|
||||||
|
@ -1184,6 +1206,7 @@ def problem_submit(request, problem, submission=None):
|
||||||
"output_only": problem.data_files.output_only
|
"output_only": problem.data_files.output_only
|
||||||
if hasattr(problem, "data_files")
|
if hasattr(problem, "data_files")
|
||||||
else False,
|
else False,
|
||||||
|
"next_valid_submit_time": next_valid_submit_time,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1208,7 +1231,7 @@ class ProblemClone(
|
||||||
problem.ac_rate = 0
|
problem.ac_rate = 0
|
||||||
problem.user_count = 0
|
problem.user_count = 0
|
||||||
problem.code = form.cleaned_data["code"]
|
problem.code = form.cleaned_data["code"]
|
||||||
problem.save()
|
problem.save(should_move_data=False)
|
||||||
problem.authors.add(self.request.profile)
|
problem.authors.add(self.request.profile)
|
||||||
problem.allowed_languages.set(languages)
|
problem.allowed_languages.set(languages)
|
||||||
problem.language_limits.set(language_limits)
|
problem.language_limits.set(language_limits)
|
||||||
|
|
|
@ -89,7 +89,7 @@ class ProblemDataForm(ModelForm):
|
||||||
"checker",
|
"checker",
|
||||||
"checker_args",
|
"checker_args",
|
||||||
"custom_checker",
|
"custom_checker",
|
||||||
"custom_validator",
|
"custom_checker_cpp",
|
||||||
"interactive_judge",
|
"interactive_judge",
|
||||||
"fileio_input",
|
"fileio_input",
|
||||||
"fileio_output",
|
"fileio_output",
|
||||||
|
@ -344,7 +344,7 @@ def problem_init_view(request, problem):
|
||||||
"problem/yaml.html",
|
"problem/yaml.html",
|
||||||
{
|
{
|
||||||
"raw_source": data,
|
"raw_source": data,
|
||||||
"highlighted_source": highlight_code(data, "yaml", linenos=False),
|
"highlighted_source": highlight_code(data, "yaml", linenos=True),
|
||||||
"title": _("Generated init.yml for %s") % problem.name,
|
"title": _("Generated init.yml for %s") % problem.name,
|
||||||
"content_title": mark_safe(
|
"content_title": mark_safe(
|
||||||
escape(_("Generated init.yml for %s"))
|
escape(_("Generated init.yml for %s"))
|
||||||
|
|
|
@ -78,12 +78,12 @@ class ManageProblemSubmissionView(TitleMixin, ManageProblemSubmissionMixin, Deta
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
context["results"] = sorted(map(itemgetter(0), Submission.RESULT))
|
context["results"] = sorted(map(itemgetter(0), Submission.RESULT))
|
||||||
context["in_contest"] = False
|
context["current_contest"] = None
|
||||||
if (
|
if (
|
||||||
self.request.in_contest_mode
|
self.request.in_contest_mode
|
||||||
and self.object in self.request.participation.contest.problems.all()
|
and self.object in self.request.participation.contest.problems.all()
|
||||||
):
|
):
|
||||||
context["in_contest"] = True
|
context["current_contest"] = self.request.participation.contest
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -106,20 +106,12 @@ class BaseActionSubmissionsView(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
languages = list(map(int, self.request.POST.getlist("language")))
|
languages = list(map(int, self.request.POST.getlist("language")))
|
||||||
|
results = list(map(str, self.request.POST.getlist("result")))
|
||||||
|
contests = list(map(int, self.request.POST.getlist("contest")))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
contest = None
|
return self.generate_response(id_range, languages, results, contests)
|
||||||
try:
|
|
||||||
in_contest = bool(self.request.POST.get("in_contest", False))
|
|
||||||
if in_contest:
|
|
||||||
contest = self.request.participation.contest
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
return self.generate_response(
|
|
||||||
id_range, languages, self.request.POST.getlist("result"), contest
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_response(self, id_range, languages, results, contest):
|
def generate_response(self, id_range, languages, results, contest):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden, JsonResponse
|
||||||
from judge.models import Contest
|
from judge.models import Contest
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class Resolver(TemplateView):
|
||||||
hidden_subtasks = self.contest.format.get_hidden_subtasks()
|
hidden_subtasks = self.contest.format.get_hidden_subtasks()
|
||||||
num_problems = len(problems)
|
num_problems = len(problems)
|
||||||
problem_sub = [0] * num_problems
|
problem_sub = [0] * num_problems
|
||||||
sub_frozen = [0] * num_problems
|
sub_frozen = [[] for _ in range(num_problems)]
|
||||||
problems_json = {str(i): {} for i in range(1, num_problems + 1)}
|
problems_json = {str(i): {} for i in range(1, num_problems + 1)}
|
||||||
|
|
||||||
users = {}
|
users = {}
|
||||||
|
@ -126,10 +126,8 @@ class Resolver(TemplateView):
|
||||||
|
|
||||||
for i in hidden_subtasks:
|
for i in hidden_subtasks:
|
||||||
order = id_to_order[i]
|
order = id_to_order[i]
|
||||||
if hidden_subtasks[i]:
|
sub_frozen[order - 1] = list(hidden_subtasks[i])
|
||||||
sub_frozen[order - 1] = min(hidden_subtasks[i])
|
|
||||||
else:
|
|
||||||
sub_frozen[order - 1] = problem_sub[order - 1] + 1
|
|
||||||
return {
|
return {
|
||||||
"problem_sub": problem_sub,
|
"problem_sub": problem_sub,
|
||||||
"sub_frozen": sub_frozen,
|
"sub_frozen": sub_frozen,
|
||||||
|
@ -143,8 +141,15 @@ class Resolver(TemplateView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
self.contest = Contest.objects.get(key=kwargs.get("contest"))
|
return HttpResponseForbidden()
|
||||||
if self.contest.format.has_hidden_subtasks:
|
self.contest = Contest.objects.get(key=kwargs.get("contest"))
|
||||||
return super(Resolver, self).get(request, *args, **kwargs)
|
if not self.contest.format.has_hidden_subtasks:
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
if self.request.GET.get("json"):
|
||||||
|
json_dumps_params = {"ensure_ascii": False}
|
||||||
|
return JsonResponse(
|
||||||
|
self.get_contest_json(), json_dumps_params=json_dumps_params
|
||||||
|
)
|
||||||
|
return super(Resolver, self).get(request, *args, **kwargs)
|
||||||
|
|
|
@ -85,15 +85,17 @@ class ProblemSelect2View(Select2View):
|
||||||
|
|
||||||
|
|
||||||
class ContestSelect2View(Select2View):
|
class ContestSelect2View(Select2View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.problem_id = kwargs.get("problem_id", request.GET.get("problem_id", ""))
|
||||||
|
return super(ContestSelect2View, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Contest.get_visible_contests(self.request.user).filter(
|
q = Contest.get_visible_contests(self.request.user).filter(
|
||||||
Q(key__icontains=self.term) | Q(name__icontains=self.term)
|
Q(key__icontains=self.term) | Q(name__icontains=self.term)
|
||||||
)
|
)
|
||||||
|
if self.problem_id:
|
||||||
|
q = q.filter(problems=self.problem_id)
|
||||||
class CommentSelect2View(Select2View):
|
return q
|
||||||
def get_queryset(self):
|
|
||||||
return Comment.objects.filter(page__icontains=self.term)
|
|
||||||
|
|
||||||
|
|
||||||
class UserSearchSelect2View(BaseListView):
|
class UserSearchSelect2View(BaseListView):
|
||||||
|
@ -193,3 +195,17 @@ class ChatUserSearchSelect2View(UserSearchSelect2View):
|
||||||
),
|
),
|
||||||
"display_rank": display_rank,
|
"display_rank": display_rank,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProblemAuthorSearchSelect2View(UserSearchSelect2View):
|
||||||
|
def get_queryset(self):
|
||||||
|
return Profile.objects.filter(
|
||||||
|
authored_problems__isnull=False, user__username__icontains=self.term
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
def get_json_result_from_object(self, user_tuple):
|
||||||
|
pk, username, email, display_rank, profile_image = user_tuple
|
||||||
|
return {
|
||||||
|
"text": username,
|
||||||
|
"id": pk,
|
||||||
|
}
|
||||||
|
|
|
@ -33,29 +33,31 @@ from django.views import View
|
||||||
|
|
||||||
from judge import event_poster as event
|
from judge import event_poster as event
|
||||||
from judge.highlight_code import highlight_code
|
from judge.highlight_code import highlight_code
|
||||||
from judge.models import Contest, ContestParticipation
|
from judge.models import (
|
||||||
from judge.models import Language
|
Contest,
|
||||||
from judge.models import Problem
|
ContestParticipation,
|
||||||
from judge.models import ProblemTestCase
|
Language,
|
||||||
from judge.models import ProblemTranslation
|
Problem,
|
||||||
from judge.models import Profile
|
ProblemTestCase,
|
||||||
from judge.models import Submission
|
ProblemTranslation,
|
||||||
|
Profile,
|
||||||
|
Submission,
|
||||||
|
)
|
||||||
from judge.utils.problems import get_result_data
|
from judge.utils.problems import get_result_data
|
||||||
from judge.utils.problems import user_completed_ids, user_editable_ids, user_tester_ids
|
|
||||||
from judge.utils.problem_data import get_problem_case
|
from judge.utils.problem_data import get_problem_case
|
||||||
from judge.utils.raw_sql import join_sql_subquery, use_straight_join
|
from judge.utils.raw_sql import join_sql_subquery, use_straight_join
|
||||||
from judge.utils.views import DiggPaginatorMixin
|
from judge.utils.views import DiggPaginatorMixin
|
||||||
from judge.utils.infinite_paginator import InfinitePaginationMixin
|
from judge.utils.infinite_paginator import InfinitePaginationMixin
|
||||||
from judge.utils.views import TitleMixin
|
from judge.utils.views import TitleMixin
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
from judge.views.contests import ContestMixin
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
def submission_related(queryset):
|
def submission_related(queryset):
|
||||||
return queryset.select_related("user__user", "problem", "language").only(
|
return queryset.select_related("user", "problem", "language").only(
|
||||||
"id",
|
"id",
|
||||||
"user__user__username",
|
"user__id",
|
||||||
"user__display_rank",
|
|
||||||
"user__rating",
|
|
||||||
"problem__name",
|
"problem__name",
|
||||||
"problem__code",
|
"problem__code",
|
||||||
"problem__is_public",
|
"problem__is_public",
|
||||||
|
@ -70,7 +72,8 @@ def submission_related(queryset):
|
||||||
"case_points",
|
"case_points",
|
||||||
"case_total",
|
"case_total",
|
||||||
"current_testcase",
|
"current_testcase",
|
||||||
"contest_object",
|
"contest_object__key",
|
||||||
|
"contest_object__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,6 +84,10 @@ class SubmissionMixin(object):
|
||||||
|
|
||||||
|
|
||||||
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
|
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
|
||||||
|
queryset = Submission.objects.select_related(
|
||||||
|
"language", "problem", "user", "contest_object"
|
||||||
|
).defer("problem__description", "user__about", "contest_object__description")
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
submission = super(SubmissionDetailBase, self).get_object(queryset)
|
submission = super(SubmissionDetailBase, self).get_object(queryset)
|
||||||
if submission.is_accessible_by(self.request.profile):
|
if submission.is_accessible_by(self.request.profile):
|
||||||
|
@ -92,7 +99,7 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta
|
||||||
submission = self.object
|
submission = self.object
|
||||||
return _("Submission of %(problem)s by %(user)s") % {
|
return _("Submission of %(problem)s by %(user)s") % {
|
||||||
"problem": submission.problem.translated_name(self.request.LANGUAGE_CODE),
|
"problem": submission.problem.translated_name(self.request.LANGUAGE_CODE),
|
||||||
"user": submission.user.user.username,
|
"user": submission.user.username,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_content_title(self):
|
def get_content_title(self):
|
||||||
|
@ -107,29 +114,13 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta
|
||||||
),
|
),
|
||||||
"user": format_html(
|
"user": format_html(
|
||||||
'<a href="{0}">{1}</a>',
|
'<a href="{0}">{1}</a>',
|
||||||
reverse("user_page", args=[submission.user.user.username]),
|
reverse("user_page", args=[submission.user.username]),
|
||||||
submission.user.user.username,
|
submission.user.username,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubmissionSource(SubmissionDetailBase):
|
|
||||||
template_name = "submission/source.html"
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().select_related("source")
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(SubmissionSource, self).get_context_data(**kwargs)
|
|
||||||
submission = self.object
|
|
||||||
context["raw_source"] = submission.source.source.rstrip("\n")
|
|
||||||
context["highlighted_source"] = highlight_code(
|
|
||||||
submission.source.source, submission.language.pygments, linenos=False
|
|
||||||
)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def get_hidden_subtasks(request, submission):
|
def get_hidden_subtasks(request, submission):
|
||||||
contest = submission.contest_object
|
contest = submission.contest_object
|
||||||
if contest and contest.is_editable_by(request.user):
|
if contest and contest.is_editable_by(request.user):
|
||||||
|
@ -205,15 +196,28 @@ def get_cases_data(submission):
|
||||||
class SubmissionStatus(SubmissionDetailBase):
|
class SubmissionStatus(SubmissionDetailBase):
|
||||||
template_name = "submission/status.html"
|
template_name = "submission/status.html"
|
||||||
|
|
||||||
def access_testcases_in_contest(self):
|
def can_see_testcases(self):
|
||||||
contest = self.object.contest_or_none
|
contest_submission = self.object.contest_or_none
|
||||||
if contest is None:
|
if contest_submission is None:
|
||||||
return False
|
|
||||||
if contest.problem.problem.is_editable_by(self.request.user):
|
|
||||||
return True
|
return True
|
||||||
if contest.problem.contest.is_in_contest(self.request.user):
|
|
||||||
|
contest_problem = contest_submission.problem
|
||||||
|
problem = self.object.problem
|
||||||
|
contest = self.object.contest_object
|
||||||
|
|
||||||
|
if contest_problem.show_testcases:
|
||||||
|
return True
|
||||||
|
if problem.is_editable_by(self.request.user):
|
||||||
|
return True
|
||||||
|
if contest.is_editable_by(self.request.user):
|
||||||
|
return True
|
||||||
|
if not problem.is_public:
|
||||||
return False
|
return False
|
||||||
if contest.participation.ended:
|
if contest.is_in_contest(self.request.user):
|
||||||
|
return False
|
||||||
|
if not contest.ended:
|
||||||
|
return False
|
||||||
|
if contest_submission.participation.ended:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -228,19 +232,14 @@ class SubmissionStatus(SubmissionDetailBase):
|
||||||
)
|
)
|
||||||
context["time_limit"] = submission.problem.time_limit
|
context["time_limit"] = submission.problem.time_limit
|
||||||
context["can_see_testcases"] = False
|
context["can_see_testcases"] = False
|
||||||
context["raw_source"] = submission.source.source.rstrip("\n")
|
|
||||||
context["highlighted_source"] = highlight_code(
|
context["highlighted_source"] = highlight_code(
|
||||||
submission.source.source, submission.language.pygments, linenos=False
|
submission.source.source,
|
||||||
|
submission.language.pygments,
|
||||||
|
linenos=True,
|
||||||
|
title=submission.language,
|
||||||
)
|
)
|
||||||
|
|
||||||
contest = submission.contest_or_none
|
if self.can_see_testcases():
|
||||||
show_testcases = False
|
|
||||||
can_see_testcases = self.access_testcases_in_contest()
|
|
||||||
|
|
||||||
if contest is not None:
|
|
||||||
show_testcases = contest.problem.show_testcases or False
|
|
||||||
|
|
||||||
if contest is None or show_testcases or can_see_testcases:
|
|
||||||
context["cases_data"] = get_cases_data(submission)
|
context["cases_data"] = get_cases_data(submission)
|
||||||
context["can_see_testcases"] = True
|
context["can_see_testcases"] = True
|
||||||
try:
|
try:
|
||||||
|
@ -266,7 +265,7 @@ class SubmissionTestCaseQuery(SubmissionStatus):
|
||||||
return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs)
|
return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SubmissionSourceRaw(SubmissionSource):
|
class SubmissionSourceRaw(SubmissionDetailBase):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
submission = self.get_object()
|
submission = self.get_object()
|
||||||
return HttpResponse(submission.source.source, content_type="text/plain")
|
return HttpResponse(submission.source.source, content_type="text/plain")
|
||||||
|
@ -311,6 +310,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
||||||
def access_check(self, request):
|
def access_check(self, request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def hide_contest_in_row(self):
|
||||||
|
return self.request.in_contest_mode
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def in_contest(self):
|
def in_contest(self):
|
||||||
return (
|
return (
|
||||||
|
@ -379,17 +381,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.selected_languages:
|
if self.selected_languages:
|
||||||
# Note (DMOJ): MariaDB can't optimize this subquery for some insane, unknown reason,
|
queryset = queryset.filter(language__in=self.selected_languages)
|
||||||
# so we are forcing an eager evaluation to get the IDs right here.
|
|
||||||
# Otherwise, with multiple language filters, MariaDB refuses to use an index
|
|
||||||
# (or runs the subquery for every submission, which is even more horrifying to think about).
|
|
||||||
queryset = queryset.filter(
|
|
||||||
language__in=list(
|
|
||||||
Language.objects.filter(
|
|
||||||
key__in=self.selected_languages
|
|
||||||
).values_list("id", flat=True)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if self.selected_statuses:
|
if self.selected_statuses:
|
||||||
submission_results = [i for i, _ in Submission.RESULT]
|
submission_results = [i for i, _ in Submission.RESULT]
|
||||||
if self.selected_statuses[0] in submission_results:
|
if self.selected_statuses[0] in submission_results:
|
||||||
|
@ -460,7 +452,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
||||||
context["show_problem"] = self.show_problem
|
context["show_problem"] = self.show_problem
|
||||||
context["profile"] = self.request.profile
|
context["profile"] = self.request.profile
|
||||||
context["all_languages"] = Language.objects.all().values_list("key", "name")
|
context["all_languages"] = Language.objects.all().values_list("key", "name")
|
||||||
context["selected_languages"] = self.selected_languages
|
context["selected_languages"] = self.selected_languages_key
|
||||||
context["all_statuses"] = self.get_searchable_status_codes()
|
context["all_statuses"] = self.get_searchable_status_codes()
|
||||||
context["selected_statuses"] = self.selected_statuses
|
context["selected_statuses"] = self.selected_statuses
|
||||||
|
|
||||||
|
@ -480,11 +472,16 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
||||||
context["friend_submissions_link"] = self.get_friend_submissions_page()
|
context["friend_submissions_link"] = self.get_friend_submissions_page()
|
||||||
context["all_submissions_link"] = self.get_all_submissions_page()
|
context["all_submissions_link"] = self.get_all_submissions_page()
|
||||||
context["page_type"] = self.page_type
|
context["page_type"] = self.page_type
|
||||||
|
context["hide_contest_in_row"] = self.hide_contest_in_row()
|
||||||
|
|
||||||
context["in_hidden_subtasks_contest"] = self.in_hidden_subtasks_contest()
|
context["in_hidden_subtasks_contest"] = self.in_hidden_subtasks_contest()
|
||||||
if context["in_hidden_subtasks_contest"]:
|
if context["in_hidden_subtasks_contest"]:
|
||||||
for submission in context["submissions"]:
|
for submission in context["submissions"]:
|
||||||
self.modify_attrs(submission)
|
self.modify_attrs(submission)
|
||||||
|
context[
|
||||||
|
"is_in_editable_contest"
|
||||||
|
] = self.in_contest and self.contest.is_editable_by(self.request.user)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -494,6 +491,19 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
||||||
|
|
||||||
self.selected_languages = request.GET.getlist("language")
|
self.selected_languages = request.GET.getlist("language")
|
||||||
self.selected_statuses = request.GET.getlist("status")
|
self.selected_statuses = request.GET.getlist("status")
|
||||||
|
self.selected_languages_key = []
|
||||||
|
|
||||||
|
if self.selected_languages:
|
||||||
|
languages = Language.objects.filter(key__in=self.selected_languages).values(
|
||||||
|
"id", "key"
|
||||||
|
)
|
||||||
|
self.selected_languages = [i["id"] for i in languages]
|
||||||
|
self.selected_languages_key = [i["key"] for i in languages]
|
||||||
|
if self.selected_statuses:
|
||||||
|
allowed_statuses = [i for i, _ in Submission.RESULT + Submission.STATUS]
|
||||||
|
self.selected_statuses = [
|
||||||
|
i for i in self.selected_statuses if i in allowed_statuses
|
||||||
|
]
|
||||||
|
|
||||||
if self.in_contest and self.contest.is_editable_by(self.request.user):
|
if self.in_contest and self.contest.is_editable_by(self.request.user):
|
||||||
self.include_frozen = True
|
self.include_frozen = True
|
||||||
|
@ -736,6 +746,11 @@ def single_submission(request, submission_id, show_problem=True):
|
||||||
submission_related(Submission.objects.all()), id=int(submission_id)
|
submission_related(Submission.objects.all()), id=int(submission_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_in_editable_contest = False
|
||||||
|
if authenticated and request.in_contest_mode:
|
||||||
|
contest = request.profile.current_contest.contest
|
||||||
|
is_in_editable_contest = contest.is_editable_by(request.user)
|
||||||
|
|
||||||
if not submission.problem.is_accessible_by(request.user):
|
if not submission.problem.is_accessible_by(request.user):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
|
@ -748,6 +763,7 @@ def single_submission(request, submission_id, show_problem=True):
|
||||||
"problem_name": show_problem
|
"problem_name": show_problem
|
||||||
and submission.problem.translated_name(request.LANGUAGE_CODE),
|
and submission.problem.translated_name(request.LANGUAGE_CODE),
|
||||||
"profile": request.profile if authenticated else None,
|
"profile": request.profile if authenticated else None,
|
||||||
|
"is_in_editable_contest": is_in_editable_contest,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -783,28 +799,9 @@ class AllSubmissions(InfinitePaginationMixin, GeneralSubmissions):
|
||||||
if self.request.organization or self.in_contest:
|
if self.request.organization or self.in_contest:
|
||||||
return super(AllSubmissions, self)._get_result_data()
|
return super(AllSubmissions, self)._get_result_data()
|
||||||
|
|
||||||
key = "global_submission_result_data"
|
return _get_global_submission_result_data(
|
||||||
if self.selected_statuses:
|
self.selected_statuses, self.selected_languages
|
||||||
key += ":" + ",".join(self.selected_statuses)
|
)
|
||||||
if self.selected_languages:
|
|
||||||
key += ":" + ",".join(self.selected_languages)
|
|
||||||
result = cache.get(key)
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
queryset = Submission.objects
|
|
||||||
if self.selected_languages:
|
|
||||||
queryset = queryset.filter(
|
|
||||||
language__in=Language.objects.filter(key__in=self.selected_languages)
|
|
||||||
)
|
|
||||||
if self.selected_statuses:
|
|
||||||
submission_results = [i for i, _ in Submission.RESULT]
|
|
||||||
if self.selected_statuses[0] in submission_results:
|
|
||||||
queryset = queryset.filter(result__in=self.selected_statuses)
|
|
||||||
else:
|
|
||||||
queryset = queryset.filter(status__in=self.selected_statuses)
|
|
||||||
result = get_result_data(queryset)
|
|
||||||
cache.set(key, result, self.stats_update_interval)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class ForceContestMixin(object):
|
class ForceContestMixin(object):
|
||||||
|
@ -842,6 +839,38 @@ class ForceContestMixin(object):
|
||||||
return super(ForceContestMixin, self).get(request, *args, **kwargs)
|
return super(ForceContestMixin, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ContestSubmissions(
|
||||||
|
LoginRequiredMixin, ContestMixin, ForceContestMixin, SubmissionsListBase
|
||||||
|
):
|
||||||
|
check_contest_in_access_check = True
|
||||||
|
template_name = "contest/submissions.html"
|
||||||
|
context_object_name = "submissions"
|
||||||
|
|
||||||
|
def hide_contest_in_row(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def access_check(self, request):
|
||||||
|
super().contest_access_check(self.contest)
|
||||||
|
super().access_check(request)
|
||||||
|
|
||||||
|
def get_title(self):
|
||||||
|
return _("Submissions in") + " " + self.contest.name
|
||||||
|
|
||||||
|
def get_content_title(self):
|
||||||
|
return format_html(
|
||||||
|
_('Submissions in <a href="{0}">{1}</a>'),
|
||||||
|
reverse("contest_view", args=[self.contest.key]),
|
||||||
|
self.contest.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
self.object = self.contest
|
||||||
|
context = super(ContestSubmissions, self).get_context_data(**kwargs)
|
||||||
|
context["contest"] = self.contest
|
||||||
|
context["page_type"] = "submissions"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions):
|
class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions):
|
||||||
check_contest_in_access_check = True
|
check_contest_in_access_check = True
|
||||||
|
|
||||||
|
@ -1027,3 +1056,19 @@ class SubmissionSourceFileView(View):
|
||||||
response["Content-Type"] = "application/octet-stream"
|
response["Content-Type"] = "application/octet-stream"
|
||||||
response["Content-Disposition"] = "attachment; filename=%s" % (filename,)
|
response["Content-Disposition"] = "attachment; filename=%s" % (filename,)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gsrd", timeout=3600, expected_type=dict)
|
||||||
|
def _get_global_submission_result_data(statuses, languages):
|
||||||
|
queryset = Submission.objects
|
||||||
|
if languages:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
language__in=Language.objects.filter(id__in=languages)
|
||||||
|
)
|
||||||
|
if statuses:
|
||||||
|
submission_results = [i for i, _ in Submission.RESULT]
|
||||||
|
if statuses[0] in submission_results:
|
||||||
|
queryset = queryset.filter(result__in=statuses)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(status__in=statuses)
|
||||||
|
return get_result_data(queryset)
|
||||||
|
|
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