From af5bee5147cc427cda73184300c138b04512485a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 15:36:38 -0500 Subject: [PATCH 01/42] Update emails --- dmoj/urls.py | 20 +- judge/migrations/0163_email_change.py | 18 ++ judge/models/profile.py | 1 + judge/utils/email_render.py | 20 ++ judge/views/email.py | 104 ++++++ judge/views/register.py | 22 +- locale/vi/LC_MESSAGES/django.po | 300 +++++++++++++----- templates/email_change/email_change.html | 21 ++ .../email_change/email_change_failure.html | 4 + .../email_change/email_change_pending.html | 6 + .../email_change/email_change_success.html | 4 + templates/general_email.html | 21 ++ templates/pagedown.html | 2 +- templates/registration/activation_email.html | 29 +- .../registration/password_reset_email.html | 25 +- .../registration/registration_complete.html | 2 +- templates/registration/registration_form.html | 12 - templates/user/edit-profile.html | 40 ++- 18 files changed, 481 insertions(+), 170 deletions(-) create mode 100644 judge/migrations/0163_email_change.py create mode 100644 judge/utils/email_render.py create mode 100644 judge/views/email.py create mode 100644 templates/email_change/email_change.html create mode 100644 templates/email_change/email_change_failure.html create mode 100644 templates/email_change/email_change_pending.html create mode 100644 templates/email_change/email_change_success.html create mode 100644 templates/general_email.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 54a7f12..402a569 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -65,6 +65,7 @@ from judge.views import ( internal, resolver, course, + email, ) from judge.views.problem_data import ( ProblemDataView, @@ -104,19 +105,19 @@ register_patterns = [ # confusing 404. url( r"^activate/(?P\w+)/$", - ActivationView.as_view(title="Activation key invalid"), + ActivationView.as_view(title=_("Activation key invalid")), name="registration_activate", ), url( r"^register/$", - RegistrationView.as_view(title="Register"), + RegistrationView.as_view(title=_("Register")), name="registration_register", ), url( r"^register/complete/$", TitledTemplateView.as_view( template_name="registration/registration_complete.html", - title="Registration Completed", + title=_("Registration Completed"), ), name="registration_complete", ), @@ -124,7 +125,7 @@ register_patterns = [ r"^register/closed/$", TitledTemplateView.as_view( template_name="registration/registration_closed.html", - title="Registration not allowed", + title=_("Registration not allowed"), ), name="registration_disallowed", ), @@ -183,6 +184,17 @@ register_patterns = [ ), name="password_reset_done", ), + url(r"^email/change/$", email.email_change_view, name="email_change"), + url( + r"^email/change/verify/(?P[0-9A-Za-z]+)-(?P.+)/$", + email.verify_email_view, + name="email_change_verify", + ), + url( + r"^email/change/pending$", + email.email_change_pending_view, + name="email_change_pending", + ), url(r"^social/error/$", register.social_auth_error, name="social_auth_error"), url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"), url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"), diff --git a/judge/migrations/0163_email_change.py b/judge/migrations/0163_email_change.py new file mode 100644 index 0000000..985121d --- /dev/null +++ b/judge/migrations/0163_email_change.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-08-25 00:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0162_profile_image"), + ] + + operations = [ + migrations.AddField( + model_name="profile", + name="email_change_pending", + field=models.EmailField(blank=True, max_length=254, null=True), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index 8c8a31d..eb3572a 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -237,6 +237,7 @@ class Profile(models.Model): help_text=_("Notes for administrators regarding this user."), ) profile_image = models.ImageField(upload_to=profile_image_path, null=True) + email_change_pending = models.EmailField(blank=True, null=True) @cached_property def organization(self): diff --git a/judge/utils/email_render.py b/judge/utils/email_render.py new file mode 100644 index 0000000..5d790f5 --- /dev/null +++ b/judge/utils/email_render.py @@ -0,0 +1,20 @@ +from django.template.loader import render_to_string +from django.contrib.sites.shortcuts import get_current_site +from django.conf import settings + + +def render_email_message(request, contexts): + current_site = get_current_site(request) + email_contexts = { + "username": request.user.username, + "domain": current_site.domain, + "protocol": "https" if request.is_secure() else "http", + "site_name": settings.SITE_NAME, + "message": None, + "title": None, + "button_text": "Click here", + "url_path": None, + } + email_contexts.update(contexts) + message = render_to_string("general_email.html", email_contexts) + return message diff --git a/judge/views/email.py b/judge/views/email.py new file mode 100644 index 0000000..ede173a --- /dev/null +++ b/judge/views/email.py @@ -0,0 +1,104 @@ +from django.contrib.auth.tokens import default_token_generator +from django.core.mail import send_mail +from django.shortcuts import render, redirect +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes, force_text +from django.conf import settings +from django import forms +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User + +from urllib.parse import urlencode, urlunparse, urlparse + +from judge.models import Profile +from judge.utils.email_render import render_email_message + + +class EmailChangeForm(forms.Form): + new_email = forms.EmailField(label=_("New Email")) + + def clean_new_email(self): + new_email = self.cleaned_data.get("new_email") + if User.objects.filter(email=new_email).exists(): + raise forms.ValidationError(_("An account with this email already exists.")) + return new_email + + +@login_required +def email_change_view(request): + form = EmailChangeForm(request.POST or None) + + if request.method == "POST" and form.is_valid(): + new_email = request.POST.get("new_email") + user = request.user + profile = request.profile + + # Generate a token for email verification + token = default_token_generator.make_token(user) + uid = urlsafe_base64_encode(force_bytes(user.pk)) + + # Send the email to the user + subject = _(f"{settings.SITE_NAME} - Email Change Request") + email_contexts = { + "message": _( + "We have received a request to change your email to this email. Click the button below to change your email:" + ), + "title": _("Email Change"), + "button_text": _("Change Email"), + "url_path": reverse( + "email_change_verify", kwargs={"uidb64": uid, "token": token} + ), + } + message = render_email_message(request, email_contexts) + send_mail(subject, message, settings.EMAIL_HOST_USER, [new_email]) + profile.email_change_pending = new_email + profile.save() + return redirect("email_change_pending") + + return render( + request, + "email_change/email_change.html", + { + "form": form, + "title": _("Change email"), + }, + ) + + +def verify_email_view(request, uidb64, token): + try: + uid = force_text(urlsafe_base64_decode(uidb64)) + user = User.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + if user is not None and default_token_generator.check_token(user, token): + # Update the user's email address + profile = Profile.objects.get(user=user) + new_email = profile.email_change_pending + if new_email: + user.email = new_email + profile.email_change_pending = None + user.save() + profile.save() + + return render( + request, + "email_change/email_change_success.html", + {"title": _("Success"), "user": user}, + ) + + return render( + request, "email_change/email_change_failure.html", {"title": _("Invalid")} + ) + + +def email_change_pending_view(request): + return render( + request, + "email_change/email_change_pending.html", + { + "title": _("Email change pending"), + }, + ) diff --git a/judge/views/register.py b/judge/views/register.py index 76e8267..58d1d6f 100644 --- a/judge/views/register.py +++ b/judge/views/register.py @@ -15,7 +15,7 @@ from registration.backends.default.views import ( from registration.forms import RegistrationForm from sortedm2m.forms import SortedMultipleChoiceField -from judge.models import Language, Organization, Profile, TIMEZONE +from judge.models import Language, Profile, TIMEZONE from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget from judge.widgets import Select2MultipleWidget, Select2Widget @@ -43,29 +43,10 @@ class CustomRegistrationForm(RegistrationForm): empty_label=None, widget=Select2Widget(attrs={"style": "width:100%"}), ) - organizations = SortedMultipleChoiceField( - queryset=Organization.objects.filter(is_open=True), - label=_("Groups"), - required=False, - widget=Select2MultipleWidget(attrs={"style": "width:100%"}), - ) if ReCaptchaField is not None: captcha = ReCaptchaField(widget=ReCaptchaWidget()) - def clean_organizations(self): - organizations = self.cleaned_data.get("organizations") or [] - max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT - - if sum(org.is_open for org in organizations) > max_orgs: - raise forms.ValidationError( - _("You may not be part of more than {count} public groups.").format( - count=max_orgs - ) - ) - - return self.cleaned_data["organizations"] - def clean_email(self): if User.objects.filter(email=self.cleaned_data["email"]).exists(): raise forms.ValidationError( @@ -116,7 +97,6 @@ class RegistrationView(OldRegistrationView): cleaned_data = form.cleaned_data profile.timezone = cleaned_data["timezone"] profile.language = cleaned_data["language"] - profile.organizations.add(*cleaned_data["organizations"]) profile.save() return user diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 425241f..223d184 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-24 23:09+0700\n" +"POT-Creation-Date: 2023-08-26 03:32+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -21,7 +21,7 @@ msgstr "" #: chat_box/models.py:31 chat_box/models.py:54 chat_box/models.py:68 #: judge/admin/interface.py:150 judge/models/contest.py:636 #: judge/models/contest.py:845 judge/models/course.py:115 -#: judge/models/profile.py:374 judge/models/profile.py:452 +#: judge/models/profile.py:375 judge/models/profile.py:453 msgid "user" msgstr "người dùng" @@ -49,11 +49,27 @@ msgstr "Tiếng Việt" msgid "English" msgstr "" -#: dmoj/urls.py:135 +#: dmoj/urls.py:108 +msgid "Activation key invalid" +msgstr "Mã kích hoạt không hợp lệ" + +#: dmoj/urls.py:113 +msgid "Register" +msgstr "Đăng ký" + +#: dmoj/urls.py:120 +msgid "Registration Completed" +msgstr "Đăng ký hoàn thành" + +#: dmoj/urls.py:128 +msgid "Registration not allowed" +msgstr "Đăng ký không thành công" + +#: dmoj/urls.py:136 msgid "Login" msgstr "Đăng nhập" -#: dmoj/urls.py:212 templates/base.html:209 +#: dmoj/urls.py:216 templates/base.html:209 #: templates/organization/org-left-sidebar.html:2 msgid "Home" msgstr "Trang chủ" @@ -293,13 +309,13 @@ msgid "User" msgstr "Thành viên" #: judge/admin/profile.py:132 templates/registration/registration_form.html:40 -#: templates/user/import/table_csv.html:8 +#: templates/user/edit-profile.html:116 templates/user/import/table_csv.html:8 msgid "Email" msgstr "Email" #: judge/admin/profile.py:138 judge/views/register.py:36 #: templates/registration/registration_form.html:68 -#: templates/user/edit-profile.html:119 +#: templates/user/edit-profile.html:140 msgid "Timezone" msgstr "Múi giờ" @@ -497,7 +513,7 @@ msgstr "Tên đăng nhập" #: judge/forms.py:428 templates/registration/registration_form.html:46 #: templates/registration/registration_form.html:60 -#: templates/user/import/table_csv.html:5 +#: templates/user/edit-profile.html:108 templates/user/import/table_csv.html:5 msgid "Password" msgstr "Mật khẩu" @@ -1972,7 +1988,7 @@ msgid "" msgstr "Ảnh này sẽ thay thế logo mặc định khi ở trong tổ chức." #: judge/models/profile.py:136 judge/models/profile.py:165 -#: judge/models/profile.py:380 judge/models/profile.py:459 +#: judge/models/profile.py:381 judge/models/profile.py:460 msgid "organization" msgstr "" @@ -2078,35 +2094,35 @@ msgstr "ghi chú nội bộ" msgid "Notes for administrators regarding this user." msgstr "Ghi chú riêng cho quản trị viên." -#: judge/models/profile.py:367 +#: judge/models/profile.py:368 msgid "user profile" msgstr "thông tin người dùng" -#: judge/models/profile.py:368 +#: judge/models/profile.py:369 msgid "user profiles" msgstr "thông tin người dùng" -#: judge/models/profile.py:384 +#: judge/models/profile.py:385 msgid "request time" msgstr "thời gian đăng ký" -#: judge/models/profile.py:387 +#: judge/models/profile.py:388 msgid "state" msgstr "trạng thái" -#: judge/models/profile.py:394 +#: judge/models/profile.py:395 msgid "reason" msgstr "lý do" -#: judge/models/profile.py:397 +#: judge/models/profile.py:398 msgid "organization join request" msgstr "đơn đăng ký tham gia" -#: judge/models/profile.py:398 +#: judge/models/profile.py:399 msgid "organization join requests" msgstr "đơn đăng ký tham gia" -#: judge/models/profile.py:464 +#: judge/models/profile.py:465 #, fuzzy #| msgid "last seen" msgid "last visit" @@ -2839,6 +2855,55 @@ msgstr "Mô tả vấn đề" msgid "New clarification for %s" msgstr "Thông báo mới cho %s" +#: judge/views/email.py:19 +#, fuzzy +#| msgid "Email" +msgid "New Email" +msgstr "Email" + +#: judge/views/email.py:23 +#, fuzzy +#| msgid "Contest with key already exists." +msgid "An account with this email already exists." +msgstr "Mã kỳ thi đã tồn tại." + +#: judge/views/email.py:41 +#, python-brace-format +msgid "{settings.SITE_NAME} - Email Change Request" +msgstr "" + +#: judge/views/email.py:43 +msgid "" +"We have received a request to change your email to this email. Click the " +"button below to change your email:" +msgstr "" +"Chúng tôi đã nhận được yêu cầu thay đổi địa chỉ email của bạn thành địa chỉ " +"email này. Vui lòng nhấp vào nút bên dưới để thay đổi địa chỉ email của bạn:" + +#: judge/views/email.py:44 +msgid "Email Change" +msgstr "Thay đổi Email" + +#: judge/views/email.py:45 +msgid "Change Email" +msgstr "Thay đổi Email" + +#: judge/views/email.py:56 templates/user/edit-profile.html:120 +msgid "Change email" +msgstr "Thay đổi email" + +#: judge/views/email.py:76 +msgid "Success" +msgstr "Thành công" + +#: judge/views/email.py:78 +msgid "Invalid" +msgstr "Không hợp lệ" + +#: judge/views/email.py:83 +msgid "Email change pending" +msgstr "Yêu cầu thay đổi email đang đợi xác thực." + #: judge/views/error.py:17 msgid "404 error" msgstr "Lỗi 404" @@ -2902,19 +2967,17 @@ msgid "You are not allowed to edit this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." #: judge/views/organization.py:193 judge/views/organization.py:337 -#, fuzzy -#| msgid "Can't edit organization" msgid "Can't access organization" -msgstr "Không thể chỉnh sửa tổ chức" +msgstr "Không thể truy cập nhóm" #: judge/views/organization.py:194 judge/views/organization.py:338 msgid "You are not allowed to access this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:230 judge/views/register.py:48 -#: judge/views/stats.py:184 templates/contest/list.html:89 -#: templates/problem/list-base.html:97 templates/stats/site.html:33 -#: templates/user/user-left-sidebar.html:4 templates/user/user-list-tabs.html:6 +#: judge/views/organization.py:230 judge/views/stats.py:184 +#: templates/contest/list.html:89 templates/problem/list-base.html:97 +#: templates/stats/site.html:33 templates/user/user-left-sidebar.html:4 +#: templates/user/user-list-tabs.html:6 msgid "Groups" msgstr "Nhóm" @@ -2941,7 +3004,7 @@ msgstr "Bạn đã ở trong nhóm." msgid "This group is not open." msgstr "Nhóm này là nhóm kín." -#: judge/views/organization.py:510 judge/views/register.py:62 +#: judge/views/organization.py:510 #, python-brace-format msgid "You may not be part of more than {count} public groups." msgstr "Bạn không thể tham gia nhiều hơn {count} nhóm công khai." @@ -3205,11 +3268,11 @@ msgstr "Các bài nộp tốt nhất cho {0}" msgid "A username must contain letters, numbers, or underscores" msgstr "Tên đăng nhập phải chứa ký tự, chữ số, hoặc dấu gạch dưới" -#: judge/views/register.py:42 templates/user/edit-profile.html:123 +#: judge/views/register.py:42 templates/user/edit-profile.html:144 msgid "Preferred language" msgstr "Ngôn ngữ ưa thích" -#: judge/views/register.py:73 +#: judge/views/register.py:54 #, python-format msgid "" "The email address \"%s\" is already taken. Only one registration is allowed " @@ -3217,7 +3280,7 @@ msgid "" msgstr "" "Email \"%s\" đã được sử dụng. Mỗi email chỉ có thể đăng ký một tài khoản." -#: judge/views/register.py:85 +#: judge/views/register.py:66 msgid "" "Your email provider is not allowed due to history of abuse. Please use a " "reputable email provider." @@ -3225,11 +3288,11 @@ msgstr "" "Your email provider is not allowed due to history of abuse. Please use a " "reputable email provider." -#: judge/views/register.py:93 judge/views/register.py:131 +#: judge/views/register.py:74 judge/views/register.py:111 msgid "Registration" msgstr "Đăng ký" -#: judge/views/register.py:145 +#: judge/views/register.py:125 msgid "Authentication failure" msgstr "Xác thực thất bại" @@ -4185,11 +4248,11 @@ msgstr "Khôi phục kết quả" msgid "Disqualify" msgstr "Hủy kết quả" -#: templates/contest/ranking-table.html:54 templates/user/edit-profile.html:96 +#: templates/contest/ranking-table.html:54 templates/user/edit-profile.html:100 msgid "Fullname" msgstr "Tên đầy đủ" -#: templates/contest/ranking-table.html:55 templates/user/edit-profile.html:100 +#: templates/contest/ranking-table.html:55 templates/user/edit-profile.html:104 #: templates/user/import/table_csv.html:7 msgid "School" msgstr "Trường" @@ -4262,6 +4325,26 @@ msgstr "Còn" msgid "Upcoming contests" msgstr "Kỳ thi sắp diễn ra" +#: templates/email_change/email_change.html:19 +msgid "Verify Email" +msgstr "Xác thực Email" + +#: templates/email_change/email_change_failure.html:3 +msgid "Invalid reset link." +msgstr "Đường dẫn không hợp lệ" + +#: templates/email_change/email_change_pending.html:4 +msgid "An email was sent to" +msgstr "Email đã được gửi đến" + +#: templates/email_change/email_change_pending.html:4 +msgid "If you don't see it, kindly check your spam folder as well." +msgstr "Nếu bạn không tìm thấy nó, vui lòng kiểm tra thư mục spam của bạn." + +#: templates/email_change/email_change_success.html:3 +msgid "Your email was sucessfully changed to" +msgstr "Bạn đã đổi email thành công." + #: templates/feed/has_next.html:3 msgid "View more" msgstr "Xem thêm" @@ -4290,6 +4373,10 @@ msgstr "Dừng" msgid "Continue" msgstr "Tiếp tục" +#: templates/general_email.html:15 +msgid "Dear" +msgstr "Xin chào" + #: templates/internal/left-sidebar.html:3 msgid "Average speed" msgstr "Tốc độ trung bình" @@ -4505,6 +4592,10 @@ msgstr "Từ chối" msgid "Kick" msgstr "Đuổi" +#: templates/pagedown.html:9 +msgid "Update Preview" +msgstr "Cập nhật xem trước" + #: templates/problem/clone.html:37 msgid "Enter a new code for the cloned problem:" msgstr "Nhập mã bài mới cho bài tập được nhân bản:" @@ -4994,6 +5085,37 @@ msgstr "%(key)s không phải mã xác thực hợp lệ." msgid "Your account has been successfully activated." msgstr "Tài khoản được kích hoạt thành công." +#: templates/registration/activation_email.html:2 +msgid "Account activation" +msgstr "Kích hoạt tài khoản" + +#: templates/registration/activation_email.html:3 +#, python-format +msgid "" +"Thanks for registering! We're glad to have you. The last step is activating " +"your account. Please activate your account in the next %(expiration_days)d " +"days." +msgstr "" +"Cảm ơn bạn đã đăng ký! Chúng tôi rất vui được chào đón bạn. Bước cuối cùng " +"là kích hoạt tài khoản của bạn. Vui lòng kích hoạt tài khoản trong vòng " +"%(expiration_days)d ngày." + +#: templates/registration/activation_email.html:5 +msgid "Activate" +msgstr "Kích hoạt" + +#: templates/registration/activation_email.html:10 +msgid "" +"Alternatively, you can reply to this message to activate your account. Your " +"reply must keep the following text intact for this to work:" +msgstr "" +"Hoặc bạn có thể trả lời tin nhắn này để kích hoạt tài khoản của bạn. Email " +"trả lời của bạn phải giữ nguyên đoạn văn sau đây:" + +#: templates/registration/activation_email.html:16 +msgid "See you soon!" +msgstr "Hẹn sớm gặp lại bạn!" + #: templates/registration/login.html:9 msgid "Invalid username or password." msgstr "Tên đăng nhập hoặc mật khẩu không hợp lệ." @@ -5032,6 +5154,7 @@ msgid "Your password has been set. You may go ahead and log in now" msgstr "Mật khẩu đã được cập nhật. Hãy thử đăng nhập lại" #: templates/registration/password_reset_confirm.html:9 +#: templates/registration/password_reset_email.html:5 msgid "Reset Password" msgstr "Reset mật khẩu" @@ -5039,7 +5162,7 @@ msgstr "Reset mật khẩu" msgid "" "We've emailed you instructions for setting your password. You should be " "receiving them shortly." -msgstr "Kiểm tra email để xem hướng dẫn đặt mật khẩu." +msgstr "Chúng tôi đã gửi email cho bạn để đặt lại mật khẩu." #: templates/registration/password_reset_done.html:5 msgid "" @@ -5047,13 +5170,25 @@ msgid "" "you registered with, and check your spam folder." msgstr "Nếu bạn không nhận được email, hãy kiểm tra hộp thư rác (spam)." +#: templates/registration/password_reset_email.html:2 +msgid "Password Reset" +msgstr "Đặt lại mật khẩu" + +#: templates/registration/password_reset_email.html:3 +msgid "" +"We have received a request to reset your password. Click the button below to " +"reset your password:" +msgstr "" +"Chúng tôi đã nhận được yêu cầu đặt lại mật khẩu của bạn. Nhấn vào nút bên " +"dưới để đặt lại mật khẩu của bạn:" + #: templates/registration/password_reset_email.txt:1 #, python-format msgid "" "You're receiving this email because you requested a password reset for your " "user account at %(site_name)s." msgstr "" -"Bạn nhận được email này vì bạn đã yêu cầu reset mật khẩu tại %(site_name)s." +"Bạn nhận được email này vì bạn đã yêu cầu đặt lại mật khẩu tại %(site_name)s." #: templates/registration/password_reset_email.txt:3 msgid "Please go to the following page and choose a new password:" @@ -5075,7 +5210,7 @@ msgstr "%(site_name)s team" #: templates/registration/password_reset_subject.txt:1 #, python-format msgid "Password reset on %(site_name)s" -msgstr "Reset mật khẩu trên %(site_name)s" +msgstr "Đặt lại mật khẩu trên %(site_name)s" #: templates/registration/profile_creation.html:36 #: templates/registration/username_select.html:7 @@ -5089,8 +5224,9 @@ msgstr "Đăng ký hiện tại đã bị dừng. Hãy liên hệ admin." #: templates/registration/registration_complete.html:3 msgid "" "You have successfully been registered. An email has been sent to the email " -"address you provided to confirm your registration." -msgstr "Bạn đã đăng ký thành công. Kiểm tra email để hoàn thành việc xác thực." +"address you provided to confirm your registration. If you don't see it, " +"kindly check your spam folder as well." +msgstr "Bạn đã đăng ký thành công. Kiểm tra email để hoàn thành việc xác thực. Nếu bạn không tìm thấy nó, vui lòng kiểm tra thư mục spam của bạn." #: templates/registration/registration_form.html:61 msgid "(again, for confirmation)" @@ -5106,21 +5242,9 @@ msgstr "chọn từ bản đồ" #: templates/registration/registration_form.html:78 msgid "Default language" -msgstr "Ngôn ngữ ưa thích" +msgstr "Ngôn ngữ mặc định" -#: templates/registration/registration_form.html:81 -msgid "Affiliated organizations" -msgstr "Tổ chức bạn muốn tham gia" - -#: templates/registration/registration_form.html:97 -msgid "By registering, you agree to our" -msgstr "Bạn đồng ý với" - -#: templates/registration/registration_form.html:98 -msgid "Terms & Conditions" -msgstr "Điều khoản của chúng tôi" - -#: templates/registration/registration_form.html:101 +#: templates/registration/registration_form.html:89 msgid "Register!" msgstr "Đăng ký!" @@ -5128,7 +5252,7 @@ msgstr "Đăng ký!" #: templates/registration/totp_disable.html:45 #: templates/registration/totp_enable.html:83 msgid "Enter the 6-digit code generated by your app:" -msgstr "" +msgstr "Nhập mã xác thực gồm 6 chữ số từ app bạn chọn" #: templates/registration/totp_auth.html:41 #, python-format @@ -5516,43 +5640,51 @@ msgstr "Top Score" msgid "Rank" msgstr "Rank" -#: templates/user/edit-profile.html:104 -msgid "Avatar" -msgstr "Ảnh đại diện" - -#: templates/user/edit-profile.html:110 -msgid "Self-description" -msgstr "Tự giới thiệu" - -#: templates/user/edit-profile.html:118 -msgid "Select your closest major city" -msgstr "Chọn thành phố gần nhất" - -#: templates/user/edit-profile.html:127 -msgid "Editor theme" -msgstr "Giao diện cho code editor" - -#: templates/user/edit-profile.html:132 -msgid "Math engine" -msgstr "" - -#: templates/user/edit-profile.html:139 +#: templates/user/edit-profile.html:111 msgid "Change your password" msgstr "Đổi mật khẩu" -#: templates/user/edit-profile.html:150 +#: templates/user/edit-profile.html:125 +msgid "Avatar" +msgstr "Ảnh đại diện" + +#: templates/user/edit-profile.html:131 +msgid "Self-description" +msgstr "Tự giới thiệu" + +#: templates/user/edit-profile.html:139 +msgid "Select your closest major city" +msgstr "Chọn thành phố gần nhất" + +#: templates/user/edit-profile.html:148 +msgid "Editor theme" +msgstr "Giao diện cho code editor" + +#: templates/user/edit-profile.html:153 +msgid "Math engine" +msgstr "" + +#: templates/user/edit-profile.html:164 msgid "Two Factor Authentication is enabled." msgstr "Two Factor Authentication đã được kích hoạt." -#: templates/user/edit-profile.html:157 -msgid "Two Factor Authentication is disabled." -msgstr "Two Factor Authentication đã được hủy kích hoạt." +#: templates/user/edit-profile.html:168 +msgid "Disable" +msgstr "Tắt" -#: templates/user/edit-profile.html:162 +#: templates/user/edit-profile.html:171 +msgid "Two Factor Authentication is disabled." +msgstr "Two Factor Authentication chưa kích hoạt." + +#: templates/user/edit-profile.html:172 +msgid "Enable" +msgstr "Bật" + +#: templates/user/edit-profile.html:176 msgid "User-script" msgstr "" -#: templates/user/edit-profile.html:166 +#: templates/user/edit-profile.html:180 msgid "Update profile" msgstr "Cập nhật thông tin" @@ -5768,6 +5900,15 @@ msgstr "Thông tin" msgid "Check all" msgstr "Chọn tất cả" +#~ msgid "Affiliated organizations" +#~ msgstr "Tổ chức bạn muốn tham gia" + +#~ msgid "By registering, you agree to our" +#~ msgstr "Bạn đồng ý với" + +#~ msgid "Terms & Conditions" +#~ msgstr "Điều khoản của chúng tôi" + #~ msgid "Change your avatar" #~ msgstr "Đổi ảnh đại diện" @@ -5875,9 +6016,6 @@ msgstr "Chọn tất cả" #~ msgid "Color theme" #~ msgstr "Chủ đề màu sắc" -#~ msgid "Change color theme" -#~ msgstr "Đổi chủ đề màu sắc" - #~ msgid "commented on {time}" #~ msgstr "bình luận vào {time}" diff --git a/templates/email_change/email_change.html b/templates/email_change/email_change.html new file mode 100644 index 0000000..6033d18 --- /dev/null +++ b/templates/email_change/email_change.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block media %} + +{% endblock %} + +{% block body %} +
{% csrf_token %} + {{ form.as_table() }}
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/email_change/email_change_failure.html b/templates/email_change/email_change_failure.html new file mode 100644 index 0000000..aa3a348 --- /dev/null +++ b/templates/email_change/email_change_failure.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block body %} +

{{ _('Invalid reset link.') }}

+{% endblock %} \ No newline at end of file diff --git a/templates/email_change/email_change_pending.html b/templates/email_change/email_change_pending.html new file mode 100644 index 0000000..3254fab --- /dev/null +++ b/templates/email_change/email_change_pending.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block body %} + {% if request.profile.email_change_pending %} +

{{ _('An email was sent to') }} {{request.profile.email_change_pending}}. {{_('If you don\'t see it, kindly check your spam folder as well.')}}

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/email_change/email_change_success.html b/templates/email_change/email_change_success.html new file mode 100644 index 0000000..80e41e1 --- /dev/null +++ b/templates/email_change/email_change_success.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block body %} +

{{ _('Your email was sucessfully changed to') }} {{user.email}}

+{% endblock %} \ No newline at end of file diff --git a/templates/general_email.html b/templates/general_email.html new file mode 100644 index 0000000..0fcdbbb --- /dev/null +++ b/templates/general_email.html @@ -0,0 +1,21 @@ + + +
+
+ {{site_name}} +
+
+

{{title}}

+

{{_('Dear')}} {{username}},

+

{{message}}

+
+ +
\ No newline at end of file diff --git a/templates/pagedown.html b/templates/pagedown.html index ab5867a..294b77d 100644 --- a/templates/pagedown.html +++ b/templates/pagedown.html @@ -6,7 +6,7 @@ {% if show_preview %}
-
Update Preview
+
{{_('Update Preview')}}
{% endif %} diff --git a/templates/registration/activation_email.html b/templates/registration/activation_email.html index f5e4eaa..25bf421 100644 --- a/templates/registration/activation_email.html +++ b/templates/registration/activation_email.html @@ -1,23 +1,16 @@ -Thanks for registering on the {{ site.name }}! We're glad to have you. -

-The last step is activating your account. Please activate your {{ SITE_NAME }} account in the next {{ expiration_days }} days. -

-Please click on the following link to activate your account: -

- http://{{ site.domain }}/accounts/activate/{{ activation_key }} -

- -Alternatively, you can reply to this message to activate your account. -Your reply must keep the following text intact for this to work: +{% set url_path = "/accounts/activate/" + activation_key %} +{% set title = _("Account activation") %} +{% set message = _("Thanks for registering! We're glad to have you. The last step is activating your account. Please activate your account in the next %(expiration_days)d days.", expiration_days=expiration_days) %} +{% set username = user.get_username() %} +{% set button_text = _("Activate") %} +{% set domain = site.domain %} +{% set protocol = "http" %} +{% include "general_email.html" %} +
+{{_("Alternatively, you can reply to this message to activate your account. Your reply must keep the following text intact for this to work:")}}
 {{ activation_key }}
 
-{% if SITE_ADMIN_EMAIL %} - See you soon! -
- If you have problems activating your account, feel free to send us an email at {{ SITE_ADMIN_EMAIL }}. -{% else %} - See you soon! -{% endif %} +{{_("See you soon!")}} diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html index 30ae59d..0b30eb3 100644 --- a/templates/registration/password_reset_email.html +++ b/templates/registration/password_reset_email.html @@ -1,19 +1,6 @@ - -
-

LQDOJ -

-
-
-
- - Forgot your password on the {{ site_name }}? Don't worry!

- To reset the password for your account "{{ user.get_username() }}", click the below button. -

- Reset password -

- {% if SITE_ADMIN_EMAIL %} - See you soon! If you have problems resetting your email, feel free to shoot us an email at {{ SITE_ADMIN_EMAIL }} - {% else %} - See you soon! - {% endif %} -
+{% set url_path = url('password_reset_confirm', uidb64=uid, token=token) %} +{% set title = _("Password Reset") %} +{% set message = _("We have received a request to reset your password. Click the button below to reset your password:") %} +{% set username = user.get_username() %} +{% set button_text = _("Reset Password") %} +{% include "general_email.html" %} \ No newline at end of file diff --git a/templates/registration/registration_complete.html b/templates/registration/registration_complete.html index 6d2b39a..b91a32d 100644 --- a/templates/registration/registration_complete.html +++ b/templates/registration/registration_complete.html @@ -1,4 +1,4 @@ {% extends "base.html" %} {% block body %} -

{{ _('You have successfully been registered. An email has been sent to the email address you provided to confirm your registration.') }}

+

{{ _('You have successfully been registered. An email has been sent to the email address you provided to confirm your registration. If you don\'t see it, kindly check your spam folder as well.') }}

{% endblock %} \ No newline at end of file diff --git a/templates/registration/registration_form.html b/templates/registration/registration_form.html index 58840b1..cf5bc30 100644 --- a/templates/registration/registration_form.html +++ b/templates/registration/registration_form.html @@ -78,12 +78,6 @@
{{ _('Default language') }}
{{ form.language }} -
{{ _('Affiliated organizations') }}
- {{ form.organizations }} - {% if form.organizations.errors %} -
{{ form.organizations.errors }}
- {% endif %} - {% if form.captcha %}
{{ form.captcha }}
{% if form.captcha.errors %} @@ -92,12 +86,6 @@ {% endif %}
- {% if tos_url %} - - {{ _('By registering, you agree to our') }} - {{ _('Terms & Conditions') }}. - - {% endif %} diff --git a/templates/user/edit-profile.html b/templates/user/edit-profile.html index 0433ffd..5ca5a88 100644 --- a/templates/user/edit-profile.html +++ b/templates/user/edit-profile.html @@ -47,6 +47,10 @@ display: flex; justify-content: center; } + + .main-info tr td { + padding-bottom: 1em; + } {% endblock %} @@ -91,7 +95,7 @@ {% csrf_token %} - +
@@ -101,8 +105,25 @@ - - + + + + + + + + + +
{{ _('Fullname') }}: {{ form_user.first_name }} {{ form_user.last_name }}
{{ _('Avatar') }}: {{ form.profile_image }} {{ _('Password') }}: + + {{ _('Change your password') }} + +
{{ _('Email') }}: + {{ request.user.email }} + + ({{ _('Change email') }}) + +
{{ _('Avatar') }}: {{ form.profile_image }}

@@ -133,29 +154,22 @@ {{ form.math_engine }} {% endif %} - - - - {{ _('Change your password') }} - - - - +
{% if profile.is_totp_enabled %} {{ _('Two Factor Authentication is enabled.') }} {% if require_staff_2fa and request.user.is_staff %} Disable {% else %} - Disable + {{_('Disable')}} {% endif %} {% else %} {{ _('Two Factor Authentication is disabled.') }} - Enable + {{_('Enable')}} {% endif %}


From b8bee7e63d97e2044241f935b83390b875f70e57 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 15:44:27 -0500 Subject: [PATCH 02/42] Add trans --- judge/views/email.py | 2 +- locale/vi/LC_MESSAGES/django.po | 41 +++++++++++-------- .../registration/activation_email_subject.txt | 2 +- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/judge/views/email.py b/judge/views/email.py index ede173a..c37f857 100644 --- a/judge/views/email.py +++ b/judge/views/email.py @@ -40,7 +40,7 @@ def email_change_view(request): uid = urlsafe_base64_encode(force_bytes(user.pk)) # Send the email to the user - subject = _(f"{settings.SITE_NAME} - Email Change Request") + subject = settings.SITE_NAME + " - " + _("Email Change Request") email_contexts = { "message": _( "We have received a request to change your email to this email. Click the button below to change your email:" diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 223d184..05a4018 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-26 03:32+0700\n" +"POT-Creation-Date: 2023-08-26 03:42+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -69,7 +69,7 @@ msgstr "Đăng ký không thành công" msgid "Login" msgstr "Đăng nhập" -#: dmoj/urls.py:216 templates/base.html:209 +#: dmoj/urls.py:224 templates/base.html:209 #: templates/organization/org-left-sidebar.html:2 msgid "Home" msgstr "Trang chủ" @@ -2855,24 +2855,23 @@ msgstr "Mô tả vấn đề" msgid "New clarification for %s" msgstr "Thông báo mới cho %s" -#: judge/views/email.py:19 +#: judge/views/email.py:20 #, fuzzy #| msgid "Email" msgid "New Email" msgstr "Email" -#: judge/views/email.py:23 +#: judge/views/email.py:25 #, fuzzy #| msgid "Contest with key already exists." msgid "An account with this email already exists." msgstr "Mã kỳ thi đã tồn tại." -#: judge/views/email.py:41 -#, python-brace-format -msgid "{settings.SITE_NAME} - Email Change Request" -msgstr "" - #: judge/views/email.py:43 +msgid "Email Change Request" +msgstr "Thay đổi Email" + +#: judge/views/email.py:46 msgid "" "We have received a request to change your email to this email. Click the " "button below to change your email:" @@ -2880,27 +2879,27 @@ msgstr "" "Chúng tôi đã nhận được yêu cầu thay đổi địa chỉ email của bạn thành địa chỉ " "email này. Vui lòng nhấp vào nút bên dưới để thay đổi địa chỉ email của bạn:" -#: judge/views/email.py:44 +#: judge/views/email.py:48 msgid "Email Change" msgstr "Thay đổi Email" -#: judge/views/email.py:45 +#: judge/views/email.py:49 msgid "Change Email" msgstr "Thay đổi Email" -#: judge/views/email.py:56 templates/user/edit-profile.html:120 +#: judge/views/email.py:65 templates/user/edit-profile.html:120 msgid "Change email" msgstr "Thay đổi email" -#: judge/views/email.py:76 +#: judge/views/email.py:89 msgid "Success" msgstr "Thành công" -#: judge/views/email.py:78 +#: judge/views/email.py:93 msgid "Invalid" msgstr "Không hợp lệ" -#: judge/views/email.py:83 +#: judge/views/email.py:102 msgid "Email change pending" msgstr "Yêu cầu thay đổi email đang đợi xác thực." @@ -5116,6 +5115,11 @@ msgstr "" msgid "See you soon!" msgstr "Hẹn sớm gặp lại bạn!" +#: templates/registration/activation_email_subject.txt:1 +#, python-format +msgid "Activate your %(SITE_NAME)s account" +msgstr "Kích hoạt tài khoản %(SITE_NAME)s" + #: templates/registration/login.html:9 msgid "Invalid username or password." msgstr "Tên đăng nhập hoặc mật khẩu không hợp lệ." @@ -5226,7 +5230,9 @@ msgid "" "You have successfully been registered. An email has been sent to the email " "address you provided to confirm your registration. If you don't see it, " "kindly check your spam folder as well." -msgstr "Bạn đã đăng ký thành công. Kiểm tra email để hoàn thành việc xác thực. Nếu bạn không tìm thấy nó, vui lòng kiểm tra thư mục spam của bạn." +msgstr "" +"Bạn đã đăng ký thành công. Kiểm tra email để hoàn thành việc xác thực. Nếu " +"bạn không tìm thấy nó, vui lòng kiểm tra thư mục spam của bạn." #: templates/registration/registration_form.html:61 msgid "(again, for confirmation)" @@ -5900,6 +5906,9 @@ msgstr "Thông tin" msgid "Check all" msgstr "Chọn tất cả" +#~ msgid "{settings.SITE_NAME} - Email Change Request" +#~ msgstr "{settings.SITE_NAME} - Thay đổi email" + #~ msgid "Affiliated organizations" #~ msgstr "Tổ chức bạn muốn tham gia" diff --git a/templates/registration/activation_email_subject.txt b/templates/registration/activation_email_subject.txt index b0888c8..5b3bd66 100644 --- a/templates/registration/activation_email_subject.txt +++ b/templates/registration/activation_email_subject.txt @@ -1 +1 @@ -Activate your {{ SITE_NAME }} account \ No newline at end of file +{% trans %}Activate your {{ SITE_NAME }} account{% endtrans %} From 0d3ebaba4710bc7773792e5a311457273f8fe666 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 15:47:20 -0500 Subject: [PATCH 03/42] Add trans --- locale/vi/LC_MESSAGES/django.po | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 05a4018..e182034 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2856,10 +2856,8 @@ msgid "New clarification for %s" msgstr "Thông báo mới cho %s" #: judge/views/email.py:20 -#, fuzzy -#| msgid "Email" msgid "New Email" -msgstr "Email" +msgstr "Email mới" #: judge/views/email.py:25 #, fuzzy From 20a8f29cd675e95bf8b74a43123bcea2b4f354f0 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 15:58:59 -0500 Subject: [PATCH 04/42] Send html for email change --- judge/views/email.py | 8 +++++++- locale/vi/LC_MESSAGES/django.po | 4 +--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/judge/views/email.py b/judge/views/email.py index c37f857..922b74f 100644 --- a/judge/views/email.py +++ b/judge/views/email.py @@ -52,7 +52,13 @@ def email_change_view(request): ), } message = render_email_message(request, email_contexts) - send_mail(subject, message, settings.EMAIL_HOST_USER, [new_email]) + send_mail( + subject, + message, + settings.EMAIL_HOST_USER, + [new_email], + html_message=message, + ) profile.email_change_pending = new_email profile.save() return redirect("email_change_pending") diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index e182034..d1e8a78 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2860,10 +2860,8 @@ msgid "New Email" msgstr "Email mới" #: judge/views/email.py:25 -#, fuzzy -#| msgid "Contest with key already exists." msgid "An account with this email already exists." -msgstr "Mã kỳ thi đã tồn tại." +msgstr "Email đã được dùng cho tài khoản khác." #: judge/views/email.py:43 msgid "Email Change Request" From d34fe197542264c293642cde1baafe21028b31f0 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 16:04:07 -0500 Subject: [PATCH 05/42] Check for email exists during verification --- judge/views/email.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/judge/views/email.py b/judge/views/email.py index 922b74f..56c43b9 100644 --- a/judge/views/email.py +++ b/judge/views/email.py @@ -80,10 +80,9 @@ def verify_email_view(request, uidb64, token): except (TypeError, ValueError, OverflowError, User.DoesNotExist): user = None if user is not None and default_token_generator.check_token(user, token): - # Update the user's email address profile = Profile.objects.get(user=user) new_email = profile.email_change_pending - if new_email: + if new_email and not User.objects.filter(email=new_email).exists(): user.email = new_email profile.email_change_pending = None user.save() From 0da2098bbe12a465ac38001993d18c7837a0e163 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 16:51:32 -0500 Subject: [PATCH 06/42] Fix datetimepicker --- judge/widgets/datetime.py | 4 ++-- resources/datetime-picker/datetimepicker.full.min.js | 1 + resources/datetime-picker/datetimepicker.min.css | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 resources/datetime-picker/datetimepicker.full.min.js create mode 100644 resources/datetime-picker/datetimepicker.min.css diff --git a/judge/widgets/datetime.py b/judge/widgets/datetime.py index 6cd7fc5..15bb383 100644 --- a/judge/widgets/datetime.py +++ b/judge/widgets/datetime.py @@ -16,8 +16,8 @@ class DateTimePickerWidget(forms.DateTimeInput): @property def media(self): - css_url = "https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css" - js_url = "https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js" + css_url = "/static/datetime-picker/datetimepicker.min.css" + js_url = "/static/datetime-picker/datetimepicker.full.min.js" return forms.Media( js=[js_url], css={"screen": [css_url]}, diff --git a/resources/datetime-picker/datetimepicker.full.min.js b/resources/datetime-picker/datetimepicker.full.min.js new file mode 100644 index 0000000..a2a09c6 --- /dev/null +++ b/resources/datetime-picker/datetimepicker.full.min.js @@ -0,0 +1 @@ +var DateFormatter;!function(){"use strict";var e,t,a,r,n,o,i;o=864e5,i=3600,e=function(e,t){return"string"==typeof e&&"string"==typeof t&&e.toLowerCase()===t.toLowerCase()},t=function(e,a,r){var n=r||"0",o=e.toString();return o.lengths?"20":"19")+i):s,h=!0;break;case"m":case"n":case"M":case"F":if(isNaN(s)){if(!((u=m.getMonth(i))>0))return null;D.month=u}else{if(!(s>=1&&12>=s))return null;D.month=s}h=!0;break;case"d":case"j":if(!(s>=1&&31>=s))return null;D.day=s,h=!0;break;case"g":case"h":if(d=r.indexOf("a")>-1?r.indexOf("a"):r.indexOf("A")>-1?r.indexOf("A"):-1,c=n[d],d>-1)l=e(c,p.meridiem[0])?0:e(c,p.meridiem[1])?12:-1,s>=1&&12>=s&&l>-1?D.hour=s+l-1:s>=0&&23>=s&&(D.hour=s);else{if(!(s>=0&&23>=s))return null;D.hour=s}g=!0;break;case"G":case"H":if(!(s>=0&&23>=s))return null;D.hour=s,g=!0;break;case"i":if(!(s>=0&&59>=s))return null;D.min=s,g=!0;break;case"s":if(!(s>=0&&59>=s))return null;D.sec=s,g=!0}if(!0===h&&D.year&&D.month&&D.day)D.date=new Date(D.year,D.month-1,D.day,D.hour,D.min,D.sec,0);else{if(!0!==g)return null;D.date=new Date(0,0,0,D.hour,D.min,D.sec,0)}return D.date},guessDate:function(e,t){if("string"!=typeof e)return e;var a,r,n,o,i,s,u=this,d=e.replace(u.separators,"\0").split("\0"),l=/^[djmn]/g,f=t.match(u.validParts),c=new Date,m=0;if(!l.test(f[0]))return e;for(n=0;na?a:4,!(r=parseInt(4>a?r.toString().substr(0,4-a)+i:i.substr(0,4))))return null;c.setFullYear(r);break;case 3:c.setHours(s);break;case 4:c.setMinutes(s);break;case 5:c.setSeconds(s)}(o=i.substr(m)).length>0&&d.splice(n+1,0,o)}return c},parseFormat:function(e,a){var r,n=this,s=n.dateSettings,u=/\\?(.?)/gi,d=function(e,t){return r[e]?r[e]():t};return r={d:function(){return t(r.j(),2)},D:function(){return s.daysShort[r.w()]},j:function(){return a.getDate()},l:function(){return s.days[r.w()]},N:function(){return r.w()||7},w:function(){return a.getDay()},z:function(){var e=new Date(r.Y(),r.n()-1,r.j()),t=new Date(r.Y(),0,1);return Math.round((e-t)/o)},W:function(){var e=new Date(r.Y(),r.n()-1,r.j()-r.N()+3),a=new Date(e.getFullYear(),0,4);return t(1+Math.round((e-a)/o/7),2)},F:function(){return s.months[a.getMonth()]},m:function(){return t(r.n(),2)},M:function(){return s.monthsShort[a.getMonth()]},n:function(){return a.getMonth()+1},t:function(){return new Date(r.Y(),r.n(),0).getDate()},L:function(){var e=r.Y();return e%4==0&&e%100!=0||e%400==0?1:0},o:function(){var e=r.n(),t=r.W();return r.Y()+(12===e&&9>t?1:1===e&&t>9?-1:0)},Y:function(){return a.getFullYear()},y:function(){return r.Y().toString().slice(-2)},a:function(){return r.A().toLowerCase()},A:function(){var e=r.G()<12?0:1;return s.meridiem[e]},B:function(){var e=a.getUTCHours()*i,r=60*a.getUTCMinutes(),n=a.getUTCSeconds();return t(Math.floor((e+r+n+i)/86.4)%1e3,3)},g:function(){return r.G()%12||12},G:function(){return a.getHours()},h:function(){return t(r.g(),2)},H:function(){return t(r.G(),2)},i:function(){return t(a.getMinutes(),2)},s:function(){return t(a.getSeconds(),2)},u:function(){return t(1e3*a.getMilliseconds(),6)},e:function(){return/\((.*)\)/.exec(String(a))[1]||"Coordinated Universal Time"},I:function(){return new Date(r.Y(),0)-Date.UTC(r.Y(),0)!=new Date(r.Y(),6)-Date.UTC(r.Y(),6)?1:0},O:function(){var e=a.getTimezoneOffset(),r=Math.abs(e);return(e>0?"-":"+")+t(100*Math.floor(r/60)+r%60,4)},P:function(){var e=r.O();return e.substr(0,3)+":"+e.substr(3,2)},T:function(){return(String(a).match(n.tzParts)||[""]).pop().replace(n.tzClip,"")||"UTC"},Z:function(){return 60*-a.getTimezoneOffset()},c:function(){return"Y-m-d\\TH:i:sP".replace(u,d)},r:function(){return"D, d M Y H:i:s O".replace(u,d)},U:function(){return a.getTime()/1e3||0}},d(e,e)},formatDate:function(e,t){var a,r,n,o,i,s=this,u="";if("string"==typeof e&&!(e=s.parseDate(e,t)))return null;if(e instanceof Date){for(n=t.length,a=0;n>a;a++)"S"!==(i=t.charAt(a))&&"\\"!==i&&(a>0&&"\\"===t.charAt(a-1)?u+=i:(o=s.parseFormat(i,e),a!==n-1&&s.intParts.test(i)&&"S"===t.charAt(a+1)&&(r=parseInt(o)||0,o+=s.dateSettings.ordinal(r)),u+=o));return u}return""}}}();var datetimepickerFactory=function(e){"use strict";function t(e,t,a){this.date=e,this.desc=t,this.style=a}var a={i18n:{ar:{months:["كانون الثاني","شباط","آذار","نيسان","مايو","حزيران","تموز","آب","أيلول","تشرين الأول","تشرين الثاني","كانون الأول"],dayOfWeekShort:["ن","ث","ع","خ","ج","س","ح"],dayOfWeek:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"]},ro:{months:["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],dayOfWeekShort:["Du","Lu","Ma","Mi","Jo","Vi","Sâ"],dayOfWeek:["Duminică","Luni","Marţi","Miercuri","Joi","Vineri","Sâmbătă"]},id:{months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],dayOfWeekShort:["Min","Sen","Sel","Rab","Kam","Jum","Sab"],dayOfWeek:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"]},is:{months:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],dayOfWeekShort:["Sun","Mán","Þrið","Mið","Fim","Fös","Lau"],dayOfWeek:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"]},bg:{months:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],dayOfWeekShort:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],dayOfWeek:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"]},fa:{months:["فروردین","اردیبهشت","خرداد","تیر","مرداد","شهریور","مهر","آبان","آذر","دی","بهمن","اسفند"],dayOfWeekShort:["یکشنبه","دوشنبه","سه شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"],dayOfWeek:["یک‌شنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنج‌شنبه","جمعه","شنبه","یک‌شنبه"]},ru:{months:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],dayOfWeekShort:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],dayOfWeek:["Воскресенье","Понедельник","Вторник","Среда","Четверг","Пятница","Суббота"]},uk:{months:["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],dayOfWeekShort:["Ндл","Пнд","Втр","Срд","Чтв","Птн","Сбт"],dayOfWeek:["Неділя","Понеділок","Вівторок","Середа","Четвер","П'ятниця","Субота"]},en:{months:["January","February","March","April","May","June","July","August","September","October","November","December"],dayOfWeekShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},el:{months:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],dayOfWeekShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayOfWeek:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"]},de:{months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],dayOfWeekShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayOfWeek:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"]},nl:{months:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],dayOfWeekShort:["zo","ma","di","wo","do","vr","za"],dayOfWeek:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"]},tr:{months:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],dayOfWeekShort:["Paz","Pts","Sal","Çar","Per","Cum","Cts"],dayOfWeek:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"]},fr:{months:["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"],dayOfWeekShort:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],dayOfWeek:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"]},es:{months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],dayOfWeekShort:["Dom","Lun","Mar","Mié","Jue","Vie","Sáb"],dayOfWeek:["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"]},th:{months:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],dayOfWeekShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayOfWeek:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัส","ศุกร์","เสาร์","อาทิตย์"]},pl:{months:["styczeń","luty","marzec","kwiecień","maj","czerwiec","lipiec","sierpień","wrzesień","październik","listopad","grudzień"],dayOfWeekShort:["nd","pn","wt","śr","cz","pt","sb"],dayOfWeek:["niedziela","poniedziałek","wtorek","środa","czwartek","piątek","sobota"]},pt:{months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],dayOfWeekShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sab"],dayOfWeek:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"]},ch:{months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayOfWeekShort:["日","一","二","三","四","五","六"]},se:{months:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],dayOfWeekShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"]},km:{months:["មករា​","កុម្ភៈ","មិនា​","មេសា​","ឧសភា​","មិថុនា​","កក្កដា​","សីហា​","កញ្ញា​","តុលា​","វិច្ឆិកា","ធ្នូ​"],dayOfWeekShort:["អាទិ​","ច័ន្ទ​","អង្គារ​","ពុធ​","ព្រហ​​","សុក្រ​","សៅរ៍"],dayOfWeek:["អាទិត្យ​","ច័ន្ទ​","អង្គារ​","ពុធ​","ព្រហស្បតិ៍​","សុក្រ​","សៅរ៍"]},kr:{months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayOfWeekShort:["일","월","화","수","목","금","토"],dayOfWeek:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"]},it:{months:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],dayOfWeekShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayOfWeek:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"]},da:{months:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],dayOfWeekShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayOfWeek:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"]},no:{months:["Januar","Februar","Mars","April","Mai","Juni","Juli","August","September","Oktober","November","Desember"],dayOfWeekShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayOfWeek:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"]},ja:{months:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeekShort:["日","月","火","水","木","金","土"],dayOfWeek:["日曜","月曜","火曜","水曜","木曜","金曜","土曜"]},vi:{months:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],dayOfWeekShort:["CN","T2","T3","T4","T5","T6","T7"],dayOfWeek:["Chủ nhật","Thứ hai","Thứ ba","Thứ tư","Thứ năm","Thứ sáu","Thứ bảy"]},sl:{months:["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],dayOfWeekShort:["Ned","Pon","Tor","Sre","Čet","Pet","Sob"],dayOfWeek:["Nedelja","Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota"]},cs:{months:["Leden","Únor","Březen","Duben","Květen","Červen","Červenec","Srpen","Září","Říjen","Listopad","Prosinec"],dayOfWeekShort:["Ne","Po","Út","St","Čt","Pá","So"]},hu:{months:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],dayOfWeekShort:["Va","Hé","Ke","Sze","Cs","Pé","Szo"],dayOfWeek:["vasárnap","hétfő","kedd","szerda","csütörtök","péntek","szombat"]},az:{months:["Yanvar","Fevral","Mart","Aprel","May","Iyun","Iyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],dayOfWeekShort:["B","Be","Ça","Ç","Ca","C","Ş"],dayOfWeek:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"]},bs:{months:["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],dayOfWeekShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],dayOfWeek:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"]},ca:{months:["Gener","Febrer","Març","Abril","Maig","Juny","Juliol","Agost","Setembre","Octubre","Novembre","Desembre"],dayOfWeekShort:["Dg","Dl","Dt","Dc","Dj","Dv","Ds"],dayOfWeek:["Diumenge","Dilluns","Dimarts","Dimecres","Dijous","Divendres","Dissabte"]},"en-GB":{months:["January","February","March","April","May","June","July","August","September","October","November","December"],dayOfWeekShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},et:{months:["Jaanuar","Veebruar","Märts","Aprill","Mai","Juuni","Juuli","August","September","Oktoober","November","Detsember"],dayOfWeekShort:["P","E","T","K","N","R","L"],dayOfWeek:["Pühapäev","Esmaspäev","Teisipäev","Kolmapäev","Neljapäev","Reede","Laupäev"]},eu:{months:["Urtarrila","Otsaila","Martxoa","Apirila","Maiatza","Ekaina","Uztaila","Abuztua","Iraila","Urria","Azaroa","Abendua"],dayOfWeekShort:["Ig.","Al.","Ar.","Az.","Og.","Or.","La."],dayOfWeek:["Igandea","Astelehena","Asteartea","Asteazkena","Osteguna","Ostirala","Larunbata"]},fi:{months:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],dayOfWeekShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayOfWeek:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"]},gl:{months:["Xan","Feb","Maz","Abr","Mai","Xun","Xul","Ago","Set","Out","Nov","Dec"],dayOfWeekShort:["Dom","Lun","Mar","Mer","Xov","Ven","Sab"],dayOfWeek:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"]},hr:{months:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],dayOfWeekShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],dayOfWeek:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"]},ko:{months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayOfWeekShort:["일","월","화","수","목","금","토"],dayOfWeek:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"]},lt:{months:["Sausio","Vasario","Kovo","Balandžio","Gegužės","Birželio","Liepos","Rugpjūčio","Rugsėjo","Spalio","Lapkričio","Gruodžio"],dayOfWeekShort:["Sek","Pir","Ant","Tre","Ket","Pen","Šeš"],dayOfWeek:["Sekmadienis","Pirmadienis","Antradienis","Trečiadienis","Ketvirtadienis","Penktadienis","Šeštadienis"]},lv:{months:["Janvāris","Februāris","Marts","Aprīlis ","Maijs","Jūnijs","Jūlijs","Augusts","Septembris","Oktobris","Novembris","Decembris"],dayOfWeekShort:["Sv","Pr","Ot","Tr","Ct","Pk","St"],dayOfWeek:["Svētdiena","Pirmdiena","Otrdiena","Trešdiena","Ceturtdiena","Piektdiena","Sestdiena"]},mk:{months:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],dayOfWeekShort:["нед","пон","вто","сре","чет","пет","саб"],dayOfWeek:["Недела","Понеделник","Вторник","Среда","Четврток","Петок","Сабота"]},mn:{months:["1-р сар","2-р сар","3-р сар","4-р сар","5-р сар","6-р сар","7-р сар","8-р сар","9-р сар","10-р сар","11-р сар","12-р сар"],dayOfWeekShort:["Дав","Мяг","Лха","Пүр","Бсн","Бям","Ням"],dayOfWeek:["Даваа","Мягмар","Лхагва","Пүрэв","Баасан","Бямба","Ням"]},"pt-BR":{months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],dayOfWeekShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayOfWeek:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"]},sk:{months:["Január","Február","Marec","Apríl","Máj","Jún","Júl","August","September","Október","November","December"],dayOfWeekShort:["Ne","Po","Ut","St","Št","Pi","So"],dayOfWeek:["Nedeľa","Pondelok","Utorok","Streda","Štvrtok","Piatok","Sobota"]},sq:{months:["Janar","Shkurt","Mars","Prill","Maj","Qershor","Korrik","Gusht","Shtator","Tetor","Nëntor","Dhjetor"],dayOfWeekShort:["Die","Hën","Mar","Mër","Enj","Pre","Shtu"],dayOfWeek:["E Diel","E Hënë","E Martē","E Mërkurë","E Enjte","E Premte","E Shtunë"]},"sr-YU":{months:["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],dayOfWeekShort:["Ned","Pon","Uto","Sre","čet","Pet","Sub"],dayOfWeek:["Nedelja","Ponedeljak","Utorak","Sreda","Četvrtak","Petak","Subota"]},sr:{months:["јануар","фебруар","март","април","мај","јун","јул","август","септембар","октобар","новембар","децембар"],dayOfWeekShort:["нед","пон","уто","сре","чет","пет","суб"],dayOfWeek:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"]},sv:{months:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],dayOfWeekShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayOfWeek:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"]},"zh-TW":{months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayOfWeekShort:["日","一","二","三","四","五","六"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]},zh:{months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayOfWeekShort:["日","一","二","三","四","五","六"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]},ug:{months:["1-ئاي","2-ئاي","3-ئاي","4-ئاي","5-ئاي","6-ئاي","7-ئاي","8-ئاي","9-ئاي","10-ئاي","11-ئاي","12-ئاي"],dayOfWeek:["يەكشەنبە","دۈشەنبە","سەيشەنبە","چارشەنبە","پەيشەنبە","جۈمە","شەنبە"]},he:{months:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],dayOfWeekShort:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],dayOfWeek:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת","ראשון"]},hy:{months:["Հունվար","Փետրվար","Մարտ","Ապրիլ","Մայիս","Հունիս","Հուլիս","Օգոստոս","Սեպտեմբեր","Հոկտեմբեր","Նոյեմբեր","Դեկտեմբեր"],dayOfWeekShort:["Կի","Երկ","Երք","Չոր","Հնգ","Ուրբ","Շբթ"],dayOfWeek:["Կիրակի","Երկուշաբթի","Երեքշաբթի","Չորեքշաբթի","Հինգշաբթի","Ուրբաթ","Շաբաթ"]},kg:{months:["Үчтүн айы","Бирдин айы","Жалган Куран","Чын Куран","Бугу","Кулжа","Теке","Баш Оона","Аяк Оона","Тогуздун айы","Жетинин айы","Бештин айы"],dayOfWeekShort:["Жек","Дүй","Шей","Шар","Бей","Жум","Ише"],dayOfWeek:["Жекшемб","Дүйшөмб","Шейшемб","Шаршемб","Бейшемби","Жума","Ишенб"]},rm:{months:["Schaner","Favrer","Mars","Avrigl","Matg","Zercladur","Fanadur","Avust","Settember","October","November","December"],dayOfWeekShort:["Du","Gli","Ma","Me","Gie","Ve","So"],dayOfWeek:["Dumengia","Glindesdi","Mardi","Mesemna","Gievgia","Venderdi","Sonda"]},ka:{months:["იანვარი","თებერვალი","მარტი","აპრილი","მაისი","ივნისი","ივლისი","აგვისტო","სექტემბერი","ოქტომბერი","ნოემბერი","დეკემბერი"],dayOfWeekShort:["კვ","ორშ","სამშ","ოთხ","ხუთ","პარ","შაბ"],dayOfWeek:["კვირა","ორშაბათი","სამშაბათი","ოთხშაბათი","ხუთშაბათი","პარასკევი","შაბათი"]}},ownerDocument:document,contentWindow:window,value:"",rtl:!1,format:"Y/m/d H:i",formatTime:"H:i",formatDate:"Y/m/d",startDate:!1,step:60,monthChangeSpinner:!0,closeOnDateSelect:!1,closeOnTimeSelect:!0,closeOnWithoutClick:!0,closeOnInputClick:!0,openOnFocus:!0,timepicker:!0,datepicker:!0,weeks:!1,defaultTime:!1,defaultDate:!1,minDate:!1,maxDate:!1,minTime:!1,maxTime:!1,minDateTime:!1,maxDateTime:!1,allowTimes:[],opened:!1,initTime:!0,inline:!1,theme:"",touchMovedThreshold:5,onSelectDate:function(){},onSelectTime:function(){},onChangeMonth:function(){},onGetWeekOfYear:function(){},onChangeYear:function(){},onChangeDateTime:function(){},onShow:function(){},onClose:function(){},onGenerate:function(){},withoutCopyright:!0,inverseButton:!1,hours12:!1,next:"xdsoft_next",prev:"xdsoft_prev",dayOfWeekStart:0,parentID:"body",timeHeightInTimePicker:25,timepickerScrollbar:!0,todayButton:!0,prevButton:!0,nextButton:!0,defaultSelect:!0,scrollMonth:!0,scrollTime:!0,scrollInput:!0,lazyInit:!1,mask:!1,validateOnBlur:!0,allowBlank:!0,yearStart:1950,yearEnd:2050,monthStart:0,monthEnd:11,style:"",id:"",fixed:!1,roundTime:"round",className:"",weekends:[],highlightedDates:[],highlightedPeriods:[],allowDates:[],allowDateRe:null,disabledDates:[],disabledWeekDays:[],yearOffset:0,beforeShowDay:null,enterLikeTab:!0,showApplyButton:!1},r=null,n=null,o="en",i={meridiem:["AM","PM"]},s=function(){var t=a.i18n[o],s={days:t.dayOfWeek,daysShort:t.dayOfWeekShort,months:t.months,monthsShort:e.map(t.months,function(e){return e.substring(0,3)})};"function"==typeof DateFormatter&&(r=n=new DateFormatter({dateSettings:e.extend({},i,s)}))},u={moment:{default_options:{format:"YYYY/MM/DD HH:mm",formatDate:"YYYY/MM/DD",formatTime:"HH:mm"},formatter:{parseDate:function(e,t){if(l(t))return n.parseDate(e,t);var a=moment(e,t);return!!a.isValid()&&a.toDate()},formatDate:function(e,t){return l(t)?n.formatDate(e,t):moment(e).format(t)},formatMask:function(e){return e.replace(/Y{4}/g,"9999").replace(/Y{2}/g,"99").replace(/M{2}/g,"19").replace(/D{2}/g,"39").replace(/H{2}/g,"29").replace(/m{2}/g,"59").replace(/s{2}/g,"59")}}}};e.datetimepicker={setLocale:function(e){var t=a.i18n[e]?e:"en";o!==t&&(o=t,s())},setDateFormatter:function(t){if("string"==typeof t&&u.hasOwnProperty(t)){var n=u[t];e.extend(a,n.default_options),r=n.formatter}else r=t}};var d={RFC_2822:"D, d M Y H:i:s O",ATOM:"Y-m-dTH:i:sP",ISO_8601:"Y-m-dTH:i:sO",RFC_822:"D, d M y H:i:s O",RFC_850:"l, d-M-y H:i:s T",RFC_1036:"D, d M y H:i:s O",RFC_1123:"D, d M Y H:i:s O",RSS:"D, d M Y H:i:s O",W3C:"Y-m-dTH:i:sP"},l=function(e){return-1!==Object.values(d).indexOf(e)};e.extend(e.datetimepicker,d),s(),window.getComputedStyle||(window.getComputedStyle=function(e){return this.el=e,this.getPropertyValue=function(t){var a=/(-([a-z]))/g;return"float"===t&&(t="styleFloat"),a.test(t)&&(t=t.replace(a,function(e,t,a){return a.toUpperCase()})),e.currentStyle[t]||null},this}),Array.prototype.indexOf||(Array.prototype.indexOf=function(e,t){var a,r;for(a=t||0,r=this.length;a'),s=e('
'),i.append(s),u.addClass("xdsoft_scroller_box").append(i),D=function(e){var t=d(e).y-c+p;t<0&&(t=0),t+s[0].offsetHeight>h&&(t=h-s[0].offsetHeight),u.trigger("scroll_element.xdsoft_scroller",[l?t/l:0])},s.on("touchstart.xdsoft_scroller mousedown.xdsoft_scroller",function(r){n||u.trigger("resize_scroll.xdsoft_scroller",[a]),c=d(r).y,p=parseInt(s.css("margin-top"),10),h=i[0].offsetHeight,"mousedown"===r.type||"touchstart"===r.type?(t.ownerDocument&&e(t.ownerDocument.body).addClass("xdsoft_noselect"),e([t.ownerDocument.body,t.contentWindow]).on("touchend mouseup.xdsoft_scroller",function a(){e([t.ownerDocument.body,t.contentWindow]).off("touchend mouseup.xdsoft_scroller",a).off("mousemove.xdsoft_scroller",D).removeClass("xdsoft_noselect")}),e(t.ownerDocument.body).on("mousemove.xdsoft_scroller",D)):(g=!0,r.stopPropagation(),r.preventDefault())}).on("touchmove",function(e){g&&(e.preventDefault(),D(e))}).on("touchend touchcancel",function(){g=!1,p=0}),u.on("scroll_element.xdsoft_scroller",function(e,t){n||u.trigger("resize_scroll.xdsoft_scroller",[t,!0]),t=t>1?1:t<0||isNaN(t)?0:t,s.css("margin-top",l*t),setTimeout(function(){r.css("marginTop",-parseInt((r[0].offsetHeight-n)*t,10))},10)}).on("resize_scroll.xdsoft_scroller",function(e,t,a){var d,f;n=u[0].clientHeight,o=r[0].offsetHeight,f=(d=n/o)*i[0].offsetHeight,d>1?s.hide():(s.show(),s.css("height",parseInt(f>10?f:10,10)),l=i[0].offsetHeight-s[0].offsetHeight,!0!==a&&u.trigger("scroll_element.xdsoft_scroller",[t||Math.abs(parseInt(r.css("marginTop"),10))/(o-n)]))}),u.on("mousewheel",function(e){var t=Math.abs(parseInt(r.css("marginTop"),10));return(t-=20*e.deltaY)<0&&(t=0),u.trigger("scroll_element.xdsoft_scroller",[t/(o-n)]),e.stopPropagation(),!1}),u.on("touchstart",function(e){f=d(e),m=Math.abs(parseInt(r.css("marginTop"),10))}),u.on("touchmove",function(e){if(f){e.preventDefault();var t=d(e);u.trigger("scroll_element.xdsoft_scroller",[(m-(t.y-f.y))/(o-n)])}}),u.on("touchend touchcancel",function(){f=!1,m=0})),u.trigger("resize_scroll.xdsoft_scroller",[a])):u.find(".xdsoft_scrollbar").hide()})},e.fn.datetimepicker=function(n,i){var s,u,d=this,l=48,f=57,c=96,m=105,h=17,g=46,p=13,D=27,v=8,y=37,b=38,k=39,x=40,T=9,S=116,M=65,w=67,O=86,W=90,_=89,F=!1,C=e.isPlainObject(n)||!n?e.extend(!0,{},a,n):e.extend(!0,{},a),P=0,Y=function(e){e.on("open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart",function t(){e.is(":disabled")||e.data("xdsoft_datetimepicker")||(clearTimeout(P),P=setTimeout(function(){e.data("xdsoft_datetimepicker")||s(e),e.off("open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart",t).trigger("open.xdsoft")},100))})};return s=function(a){function i(){var e,t=!1;return C.startDate?t=A.strToDate(C.startDate):(t=C.value||(a&&a.val&&a.val()?a.val():""))?(t=A.strToDateTime(t),C.yearOffset&&(t=new Date(t.getFullYear()-C.yearOffset,t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()))):C.defaultDate&&(t=A.strToDateTime(C.defaultDate),C.defaultTime&&(e=A.strtotime(C.defaultTime),t.setHours(e.getHours()),t.setMinutes(e.getMinutes()))),t&&A.isValidDate(t)?j.data("changed",!0):t="",t||0}function s(t){var n=function(e,t){var a=e.replace(/([\[\]\/\{\}\(\)\-\.\+]{1})/g,"\\$1").replace(/_/g,"{digit+}").replace(/([0-9]{1})/g,"{digit$1}").replace(/\{digit([0-9]{1})\}/g,"[0-$1_]{1}").replace(/\{digit[\+]\}/g,"[0-9_]{1}");return new RegExp(a).test(t)},o=function(e,a){if(!(e="string"==typeof e||e instanceof String?t.ownerDocument.getElementById(e):e))return!1;if(e.createTextRange){var r=e.createTextRange();return r.collapse(!0),r.moveEnd("character",a),r.moveStart("character",a),r.select(),!0}return!!e.setSelectionRange&&(e.setSelectionRange(a,a),!0)};t.mask&&a.off("keydown.xdsoft"),!0===t.mask&&(r.formatMask?t.mask=r.formatMask(t.format):t.mask=t.format.replace(/Y/g,"9999").replace(/F/g,"9999").replace(/m/g,"19").replace(/d/g,"39").replace(/H/g,"29").replace(/i/g,"59").replace(/s/g,"59")),"string"===e.type(t.mask)&&(n(t.mask,a.val())||(a.val(t.mask.replace(/[0-9]/g,"_")),o(a[0],0)),a.on("paste.xdsoft",function(r){var i=(r.clipboardData||r.originalEvent.clipboardData||window.clipboardData).getData("text"),s=this.value,u=this.selectionStart;return s=s.substr(0,u)+i+s.substr(u+i.length),u+=i.length,n(t.mask,s)?(this.value=s,o(this,u)):""===e.trim(s)?this.value=t.mask.replace(/[0-9]/g,"_"):a.trigger("error_input.xdsoft"),r.preventDefault(),!1}),a.on("keydown.xdsoft",function(r){var i,s=this.value,u=r.which,d=this.selectionStart,C=this.selectionEnd,P=d!==C;if(u>=l&&u<=f||u>=c&&u<=m||u===v||u===g){for(i=u===v||u===g?"_":String.fromCharCode(c<=u&&u<=m?u-l:u),u===v&&d&&!P&&(d-=1);;){var Y=t.mask.substr(d,1),A=d0;if(!(/[^0-9_]/.test(Y)&&A&&H))break;d+=u!==v||P?1:-1}if(P){var j=C-d,J=t.mask.replace(/[0-9]/g,"_"),z=J.substr(d,j).substr(1);s=s.substr(0,d)+(i+z)+s.substr(d+j)}else s=s.substr(0,d)+i+s.substr(d+1);if(""===e.trim(s))s=J;else if(d===t.mask.length)return r.preventDefault(),!1;for(d+=u===v?0:1;/[^0-9_]/.test(t.mask.substr(d,1))&&d0;)d+=u===v?0:1;n(t.mask,s)?(this.value=s,o(this,d)):""===e.trim(s)?this.value=t.mask.replace(/[0-9]/g,"_"):a.trigger("error_input.xdsoft")}else if(-1!==[M,w,O,W,_].indexOf(u)&&F||-1!==[D,b,x,y,k,S,h,T,p].indexOf(u))return!0;return r.preventDefault(),!1}))}var u,d,P,Y,A,H,j=e('
'),J=e(''),z=e('
'),I=e('
'),N=e('
'),L=e('
'),E=L.find(".xdsoft_time_box").eq(0),R=e('
'),V=e(''),B=e('
'),G=e('
'),U=!1,q=0;C.id&&j.attr("id",C.id),C.style&&j.attr("style",C.style),C.weeks&&j.addClass("xdsoft_showweeks"),C.rtl&&j.addClass("xdsoft_rtl"),j.addClass("xdsoft_"+C.theme),j.addClass(C.className),I.find(".xdsoft_month span").after(B),I.find(".xdsoft_year span").after(G),I.find(".xdsoft_month,.xdsoft_year").on("touchstart mousedown.xdsoft",function(t){var a,r,n=e(this).find(".xdsoft_select").eq(0),o=0,i=0,s=n.is(":visible");for(I.find(".xdsoft_select").hide(),A.currentTime&&(o=A.currentTime[e(this).hasClass("xdsoft_month")?"getMonth":"getFullYear"]()),n[s?"hide":"show"](),a=n.find("div.xdsoft_option"),r=0;rC.touchMovedThreshold&&(this.touchMoved=!0)};I.find(".xdsoft_select").xdsoftScroller(C).on("touchstart mousedown.xdsoft",function(e){var t=e.originalEvent;this.touchMoved=!1,this.touchStartPosition=t.touches?t.touches[0]:t,e.stopPropagation(),e.preventDefault()}).on("touchmove",".xdsoft_option",X).on("touchend mousedown.xdsoft",".xdsoft_option",function(){if(!this.touchMoved){void 0!==A.currentTime&&null!==A.currentTime||(A.currentTime=A.now());var t=A.currentTime.getFullYear();A&&A.currentTime&&A.currentTime[e(this).parent().parent().hasClass("xdsoft_monthselect")?"setMonth":"setFullYear"](e(this).data("value")),e(this).parent().parent().hide(),j.trigger("xchange.xdsoft"),C.onChangeMonth&&e.isFunction(C.onChangeMonth)&&C.onChangeMonth.call(j,A.currentTime,j.data("input")),t!==A.currentTime.getFullYear()&&e.isFunction(C.onChangeYear)&&C.onChangeYear.call(j,A.currentTime,j.data("input"))}}),j.getValue=function(){return A.getCurrentTime()},j.setOptions=function(n){var o={};C=e.extend(!0,{},C,n),n.allowTimes&&e.isArray(n.allowTimes)&&n.allowTimes.length&&(C.allowTimes=e.extend(!0,[],n.allowTimes)),n.weekends&&e.isArray(n.weekends)&&n.weekends.length&&(C.weekends=e.extend(!0,[],n.weekends)),n.allowDates&&e.isArray(n.allowDates)&&n.allowDates.length&&(C.allowDates=e.extend(!0,[],n.allowDates)),n.allowDateRe&&"[object String]"===Object.prototype.toString.call(n.allowDateRe)&&(C.allowDateRe=new RegExp(n.allowDateRe)),n.highlightedDates&&e.isArray(n.highlightedDates)&&n.highlightedDates.length&&(e.each(n.highlightedDates,function(a,n){var i,s=e.map(n.split(","),e.trim),u=new t(r.parseDate(s[0],C.formatDate),s[1],s[2]),d=r.formatDate(u.date,C.formatDate);void 0!==o[d]?(i=o[d].desc)&&i.length&&u.desc&&u.desc.length&&(o[d].desc=i+"\n"+u.desc):o[d]=u}),C.highlightedDates=e.extend(!0,[],o)),n.highlightedPeriods&&e.isArray(n.highlightedPeriods)&&n.highlightedPeriods.length&&(o=e.extend(!0,[],C.highlightedDates),e.each(n.highlightedPeriods,function(a,n){var i,s,u,d,l,f,c;if(e.isArray(n))i=n[0],s=n[1],u=n[2],c=n[3];else{var m=e.map(n.split(","),e.trim);i=r.parseDate(m[0],C.formatDate),s=r.parseDate(m[1],C.formatDate),u=m[2],c=m[3]}for(;i<=s;)d=new t(i,u,c),l=r.formatDate(i,C.formatDate),i.setDate(i.getDate()+1),void 0!==o[l]?(f=o[l].desc)&&f.length&&d.desc&&d.desc.length&&(o[l].desc=f+"\n"+d.desc):o[l]=d}),C.highlightedDates=e.extend(!0,[],o)),n.disabledDates&&e.isArray(n.disabledDates)&&n.disabledDates.length&&(C.disabledDates=e.extend(!0,[],n.disabledDates)),n.disabledWeekDays&&e.isArray(n.disabledWeekDays)&&n.disabledWeekDays.length&&(C.disabledWeekDays=e.extend(!0,[],n.disabledWeekDays)),!C.open&&!C.opened||C.inline||a.trigger("open.xdsoft"),C.inline&&(U=!0,j.addClass("xdsoft_inline"),a.after(j).hide()),C.inverseButton&&(C.next="xdsoft_prev",C.prev="xdsoft_next"),C.datepicker?z.addClass("active"):z.removeClass("active"),C.timepicker?L.addClass("active"):L.removeClass("active"),C.value&&(A.setCurrentTime(C.value),a&&a.val&&a.val(A.str)),isNaN(C.dayOfWeekStart)?C.dayOfWeekStart=0:C.dayOfWeekStart=parseInt(C.dayOfWeekStart,10)%7,C.timepickerScrollbar||E.xdsoftScroller(C,"hide"),C.minDate&&/^[\+\-](.*)$/.test(C.minDate)&&(C.minDate=r.formatDate(A.strToDateTime(C.minDate),C.formatDate)),C.maxDate&&/^[\+\-](.*)$/.test(C.maxDate)&&(C.maxDate=r.formatDate(A.strToDateTime(C.maxDate),C.formatDate)),C.minDateTime&&/^\+(.*)$/.test(C.minDateTime)&&(C.minDateTime=A.strToDateTime(C.minDateTime).dateFormat(C.formatDate)),C.maxDateTime&&/^\+(.*)$/.test(C.maxDateTime)&&(C.maxDateTime=A.strToDateTime(C.maxDateTime).dateFormat(C.formatDate)),V.toggle(C.showApplyButton),I.find(".xdsoft_today_button").css("visibility",C.todayButton?"visible":"hidden"),I.find("."+C.prev).css("visibility",C.prevButton?"visible":"hidden"),I.find("."+C.next).css("visibility",C.nextButton?"visible":"hidden"),s(C),C.validateOnBlur&&a.off("blur.xdsoft").on("blur.xdsoft",function(){if(C.allowBlank&&(!e.trim(e(this).val()).length||"string"==typeof C.mask&&e.trim(e(this).val())===C.mask.replace(/[0-9]/g,"_")))e(this).val(null),j.data("xdsoft_datetime").empty();else{var t=r.parseDate(e(this).val(),C.format);if(t)e(this).val(r.formatDate(t,C.format));else{var a=+[e(this).val()[0],e(this).val()[1]].join(""),n=+[e(this).val()[2],e(this).val()[3]].join("");!C.datepicker&&C.timepicker&&a>=0&&a<24&&n>=0&&n<60?e(this).val([a,n].map(function(e){return e>9?e:"0"+e}).join(":")):e(this).val(r.formatDate(A.now(),C.format))}j.data("xdsoft_datetime").setCurrentTime(e(this).val())}j.trigger("changedatetime.xdsoft"),j.trigger("close.xdsoft")}),C.dayOfWeekStartPrev=0===C.dayOfWeekStart?6:C.dayOfWeekStart-1,j.trigger("xchange.xdsoft").trigger("afterOpen.xdsoft")},j.data("options",C).on("touchstart mousedown.xdsoft",function(e){return e.stopPropagation(),e.preventDefault(),G.hide(),B.hide(),!1}),E.append(R),E.xdsoftScroller(C),j.on("afterOpen.xdsoft",function(){E.xdsoftScroller(C)}),j.append(z).append(L),!0!==C.withoutCopyright&&j.append(J),z.append(I).append(N).append(V),e(C.parentID).append(j),A=new function(){var t=this;t.now=function(e){var a,r,n=new Date;return!e&&C.defaultDate&&(a=t.strToDateTime(C.defaultDate),n.setFullYear(a.getFullYear()),n.setMonth(a.getMonth()),n.setDate(a.getDate())),n.setFullYear(n.getFullYear()),!e&&C.defaultTime&&(r=t.strtotime(C.defaultTime),n.setHours(r.getHours()),n.setMinutes(r.getMinutes()),n.setSeconds(r.getSeconds()),n.setMilliseconds(r.getMilliseconds())),n},t.isValidDate=function(e){return"[object Date]"===Object.prototype.toString.call(e)&&!isNaN(e.getTime())},t.setCurrentTime=function(e,a){"string"==typeof e?t.currentTime=t.strToDateTime(e):t.isValidDate(e)?t.currentTime=e:e||a||!C.allowBlank||C.inline?t.currentTime=t.now():t.currentTime=null,j.trigger("xchange.xdsoft")},t.empty=function(){t.currentTime=null},t.getCurrentTime=function(){return t.currentTime},t.nextMonth=function(){void 0!==t.currentTime&&null!==t.currentTime||(t.currentTime=t.now());var a,r=t.currentTime.getMonth()+1;return 12===r&&(t.currentTime.setFullYear(t.currentTime.getFullYear()+1),r=0),a=t.currentTime.getFullYear(),t.currentTime.setDate(Math.min(new Date(t.currentTime.getFullYear(),r+1,0).getDate(),t.currentTime.getDate())),t.currentTime.setMonth(r),C.onChangeMonth&&e.isFunction(C.onChangeMonth)&&C.onChangeMonth.call(j,A.currentTime,j.data("input")),a!==t.currentTime.getFullYear()&&e.isFunction(C.onChangeYear)&&C.onChangeYear.call(j,A.currentTime,j.data("input")),j.trigger("xchange.xdsoft"),r},t.prevMonth=function(){void 0!==t.currentTime&&null!==t.currentTime||(t.currentTime=t.now());var a=t.currentTime.getMonth()-1;return-1===a&&(t.currentTime.setFullYear(t.currentTime.getFullYear()-1),a=11),t.currentTime.setDate(Math.min(new Date(t.currentTime.getFullYear(),a+1,0).getDate(),t.currentTime.getDate())),t.currentTime.setMonth(a),C.onChangeMonth&&e.isFunction(C.onChangeMonth)&&C.onChangeMonth.call(j,A.currentTime,j.data("input")),j.trigger("xchange.xdsoft"),a},t.getWeekOfYear=function(t){if(C.onGetWeekOfYear&&e.isFunction(C.onGetWeekOfYear)){var a=C.onGetWeekOfYear.call(j,t);if(void 0!==a)return a}var r=new Date(t.getFullYear(),0,1);return 4!==r.getDay()&&r.setMonth(0,1+(4-r.getDay()+7)%7),Math.ceil(((t-r)/864e5+r.getDay()+1)/7)},t.strToDateTime=function(e){var a,n,o=[];return e&&e instanceof Date&&t.isValidDate(e)?e:((o=/^([+-]{1})(.*)$/.exec(e))&&(o[2]=r.parseDate(o[2],C.formatDate)),o&&o[2]?(a=o[2].getTime()-6e4*o[2].getTimezoneOffset(),n=new Date(t.now(!0).getTime()+parseInt(o[1]+"1",10)*a)):n=e?r.parseDate(e,C.format):t.now(),t.isValidDate(n)||(n=t.now()),n)},t.strToDate=function(e){if(e&&e instanceof Date&&t.isValidDate(e))return e;var a=e?r.parseDate(e,C.formatDate):t.now(!0);return t.isValidDate(a)||(a=t.now(!0)),a},t.strtotime=function(e){if(e&&e instanceof Date&&t.isValidDate(e))return e;var a=e?r.parseDate(e,C.formatTime):t.now(!0);return t.isValidDate(a)||(a=t.now(!0)),a},t.str=function(){var e=C.format;return C.yearOffset&&(e=(e=e.replace("Y",t.currentTime.getFullYear()+C.yearOffset)).replace("y",String(t.currentTime.getFullYear()+C.yearOffset).substring(2,4))),r.formatDate(t.currentTime,e)},t.currentTime=this.now()},V.on("touchend click",function(e){e.preventDefault(),j.data("changed",!0),A.setCurrentTime(i()),a.val(A.str()),j.trigger("close.xdsoft")}),I.find(".xdsoft_today_button").on("touchend mousedown.xdsoft",function(){j.data("changed",!0),A.setCurrentTime(0,!0),j.trigger("afterOpen.xdsoft")}).on("dblclick.xdsoft",function(){var e,t,r=A.getCurrentTime();r=new Date(r.getFullYear(),r.getMonth(),r.getDate()),e=A.strToDate(C.minDate),r<(e=new Date(e.getFullYear(),e.getMonth(),e.getDate()))||(t=A.strToDate(C.maxDate),r>(t=new Date(t.getFullYear(),t.getMonth(),t.getDate()))||(a.val(A.str()),a.trigger("change"),j.trigger("close.xdsoft")))}),I.find(".xdsoft_prev,.xdsoft_next").on("touchend mousedown.xdsoft",function(){var t=e(this),a=0,r=!1;!function e(n){t.hasClass(C.next)?A.nextMonth():t.hasClass(C.prev)&&A.prevMonth(),C.monthChangeSpinner&&(r||(a=setTimeout(e,n||100)))}(500),e([C.ownerDocument.body,C.contentWindow]).on("touchend mouseup.xdsoft",function t(){clearTimeout(a),r=!0,e([C.ownerDocument.body,C.contentWindow]).off("touchend mouseup.xdsoft",t)})}),L.find(".xdsoft_prev,.xdsoft_next").on("touchend mousedown.xdsoft",function(){var t=e(this),a=0,r=!1,n=110;!function e(o){var i=E[0].clientHeight,s=R[0].offsetHeight,u=Math.abs(parseInt(R.css("marginTop"),10));t.hasClass(C.next)&&s-i-C.timeHeightInTimePicker>=u?R.css("marginTop","-"+(u+C.timeHeightInTimePicker)+"px"):t.hasClass(C.prev)&&u-C.timeHeightInTimePicker>=0&&R.css("marginTop","-"+(u-C.timeHeightInTimePicker)+"px"),E.trigger("scroll_element.xdsoft_scroller",[Math.abs(parseInt(R[0].style.marginTop,10)/(s-i))]),n=n>10?10:n-10,r||(a=setTimeout(e,o||n))}(500),e([C.ownerDocument.body,C.contentWindow]).on("touchend mouseup.xdsoft",function t(){clearTimeout(a),r=!0,e([C.ownerDocument.body,C.contentWindow]).off("touchend mouseup.xdsoft",t)})}),u=0,j.on("xchange.xdsoft",function(t){clearTimeout(u),u=setTimeout(function(){void 0!==A.currentTime&&null!==A.currentTime||(A.currentTime=A.now());for(var t,i,s,u,d,l,f,c,m,h,g="",p=new Date(A.currentTime.getFullYear(),A.currentTime.getMonth(),1,12,0,0),D=0,v=A.now(),y=!1,b=!1,k=!1,x=!1,T=[],S=!0,M="";p.getDay()!==C.dayOfWeekStart;)p.setDate(p.getDate()-1);for(g+="",C.weeks&&(g+=""),t=0;t<7;t+=1)g+="";g+="",g+="",!1!==C.maxDate&&(y=A.strToDate(C.maxDate),y=new Date(y.getFullYear(),y.getMonth(),y.getDate(),23,59,59,999)),!1!==C.minDate&&(b=A.strToDate(C.minDate),b=new Date(b.getFullYear(),b.getMonth(),b.getDate())),!1!==C.minDateTime&&(k=A.strToDate(C.minDateTime),k=new Date(k.getFullYear(),k.getMonth(),k.getDate(),k.getHours(),k.getMinutes(),k.getSeconds())),!1!==C.maxDateTime&&(x=A.strToDate(C.maxDateTime),x=new Date(x.getFullYear(),x.getMonth(),x.getDate(),x.getHours(),x.getMinutes(),x.getSeconds()));var w;for(!1!==x&&(w=31*(12*x.getFullYear()+x.getMonth())+x.getDate());D0&&-1===C.allowDates.indexOf(r.formatDate(p,C.formatDate))&&T.push("xdsoft_disabled");var O=31*(12*p.getFullYear()+p.getMonth())+p.getDate();(!1!==y&&p>y||!1!==k&&pw||c&&!1===c[0])&&T.push("xdsoft_disabled"),-1!==C.disabledDates.indexOf(r.formatDate(p,C.formatDate))&&T.push("xdsoft_disabled"),-1!==C.disabledWeekDays.indexOf(s)&&T.push("xdsoft_disabled"),a.is("[disabled]")&&T.push("xdsoft_disabled"),c&&""!==c[1]&&T.push(c[1]),A.currentTime.getMonth()!==l&&T.push("xdsoft_other_month"),(C.defaultSelect||j.data("changed"))&&r.formatDate(A.currentTime,C.formatDate)===r.formatDate(p,C.formatDate)&&T.push("xdsoft_current"),r.formatDate(v,C.formatDate)===r.formatDate(p,C.formatDate)&&T.push("xdsoft_today"),0!==p.getDay()&&6!==p.getDay()&&-1===C.weekends.indexOf(r.formatDate(p,C.formatDate))||T.push("xdsoft_weekend"),void 0!==C.highlightedDates[r.formatDate(p,C.formatDate)]&&(i=C.highlightedDates[r.formatDate(p,C.formatDate)],T.push(void 0===i.style?"xdsoft_highlighted_default":i.style),h=void 0===i.desc?"":i.desc),C.beforeShowDay&&e.isFunction(C.beforeShowDay)&&T.push(C.beforeShowDay(p)),S&&(g+="",S=!1,C.weeks&&(g+="")),g+='",p.getDay()===C.dayOfWeekStartPrev&&(g+="",S=!0),p.setDate(u+1)}g+="
"+C.i18n[o].dayOfWeekShort[(t+C.dayOfWeekStart)%7]+"
"+f+"
'+u+"
",N.html(g),I.find(".xdsoft_label span").eq(0).text(C.i18n[o].months[A.currentTime.getMonth()]),I.find(".xdsoft_label span").eq(1).text(A.currentTime.getFullYear()+C.yearOffset),M="",l="";var W=0;if(!1!==C.minTime){F=A.strtotime(C.minTime);W=60*F.getHours()+F.getMinutes()}var _=1440;if(!1!==C.maxTime){F=A.strtotime(C.maxTime);_=60*F.getHours()+F.getMinutes()}if(!1!==C.minDateTime){F=A.strToDateTime(C.minDateTime);r.formatDate(A.currentTime,C.formatDate)===r.formatDate(F,C.formatDate)&&(l=60*F.getHours()+F.getMinutes())>W&&(W=l)}if(!1!==C.maxDateTime){var F=A.strToDateTime(C.maxDateTime);r.formatDate(A.currentTime,C.formatDate)===r.formatDate(F,C.formatDate)&&(l=60*F.getHours()+F.getMinutes())<_&&(_=l)}if(m=function(t,n){var o,i=A.now(),s=C.allowTimes&&e.isArray(C.allowTimes)&&C.allowTimes.length;i.setHours(t),t=parseInt(i.getHours(),10),i.setMinutes(n),n=parseInt(i.getMinutes(),10),T=[];var u=60*t+n;(a.is("[disabled]")||u>=_||u59||o.getMinutes()===parseInt(n,10))&&(C.defaultSelect||j.data("changed")?T.push("xdsoft_current"):C.initTime&&T.push("xdsoft_init_time")),parseInt(v.getHours(),10)===parseInt(t,10)&&parseInt(v.getMinutes(),10)===parseInt(n,10)&&T.push("xdsoft_today"),M+='
'+r.formatDate(i,C.formatTime)+"
"},C.allowTimes&&e.isArray(C.allowTimes)&&C.allowTimes.length)for(D=0;D=_||m((D<10?"0":"")+D,l=(t<10?"0":"")+t))}for(R.html(M),n="",D=parseInt(C.yearStart,10);D<=parseInt(C.yearEnd,10);D+=1)n+='
'+(D+C.yearOffset)+"
";for(G.children().eq(0).html(n),D=parseInt(C.monthStart,10),n="";D<=parseInt(C.monthEnd,10);D+=1)n+='
'+C.i18n[o].months[D]+"
";B.children().eq(0).html(n),e(j).trigger("generate.xdsoft")},10),t.stopPropagation()}).on("afterOpen.xdsoft",function(){if(C.timepicker){var e,t,a,r;R.find(".xdsoft_current").length?e=".xdsoft_current":R.find(".xdsoft_init_time").length&&(e=".xdsoft_init_time"),e?(t=E[0].clientHeight,(a=R[0].offsetHeight)-t<(r=R.find(e).index()*C.timeHeightInTimePicker+1)&&(r=a-t),E.trigger("scroll_element.xdsoft_scroller",[parseInt(r,10)/(a-t)])):E.trigger("scroll_element.xdsoft_scroller",[0])}}),d=0,N.on("touchend click.xdsoft","td",function(t){t.stopPropagation(),d+=1;var r=e(this),n=A.currentTime;if(void 0!==n&&null!==n||(A.currentTime=A.now(),n=A.currentTime),r.hasClass("xdsoft_disabled"))return!1;n.setDate(1),n.setFullYear(r.data("year")),n.setMonth(r.data("month")),n.setDate(r.data("date")),j.trigger("select.xdsoft",[n]),a.val(A.str()),C.onSelectDate&&e.isFunction(C.onSelectDate)&&C.onSelectDate.call(j,A.currentTime,j.data("input"),t),j.data("changed",!0),j.trigger("xchange.xdsoft"),j.trigger("changedatetime.xdsoft"),(d>1||!0===C.closeOnDateSelect||!1===C.closeOnDateSelect&&!C.timepicker)&&!C.inline&&j.trigger("close.xdsoft"),setTimeout(function(){d=0},200)}),R.on("touchstart","div",function(e){this.touchMoved=!1}).on("touchmove","div",X).on("touchend click.xdsoft","div",function(t){if(!this.touchMoved){t.stopPropagation();var a=e(this),r=A.currentTime;if(void 0!==r&&null!==r||(A.currentTime=A.now(),r=A.currentTime),a.hasClass("xdsoft_disabled"))return!1;r.setHours(a.data("hour")),r.setMinutes(a.data("minute")),j.trigger("select.xdsoft",[r]),j.data("input").val(A.str()),C.onSelectTime&&e.isFunction(C.onSelectTime)&&C.onSelectTime.call(j,A.currentTime,j.data("input"),t),j.data("changed",!0),j.trigger("xchange.xdsoft"),j.trigger("changedatetime.xdsoft"),!0!==C.inline&&!0===C.closeOnTimeSelect&&j.trigger("close.xdsoft")}}),z.on("mousewheel.xdsoft",function(e){return!C.scrollMonth||(e.deltaY<0?A.nextMonth():A.prevMonth(),!1)}),a.on("mousewheel.xdsoft",function(e){return!C.scrollInput||(!C.datepicker&&C.timepicker?((P=R.find(".xdsoft_current").length?R.find(".xdsoft_current").eq(0).index():0)+e.deltaY>=0&&P+e.deltaYc+m?(l="bottom",r=c+m-t.top):r-=m):r+j[0].offsetHeight>c+m&&(r=t.top-j[0].offsetHeight+1),r<0&&(r=0),n+a.offsetWidth>d&&(n=d-a.offsetWidth)),i=j[0],H(i,function(e){if("relative"===C.contentWindow.getComputedStyle(e).getPropertyValue("position")&&d>=e.offsetWidth)return n-=(d-e.offsetWidth)/2,!1}),(f={position:o,left:n,top:"",bottom:""})[l]=r,j.css(f)},j.on("open.xdsoft",function(t){var a=!0;C.onShow&&e.isFunction(C.onShow)&&(a=C.onShow.call(j,A.currentTime,j.data("input"),t)),!1!==a&&(j.show(),Y(),e(C.contentWindow).off("resize.xdsoft",Y).on("resize.xdsoft",Y),C.closeOnWithoutClick&&e([C.ownerDocument.body,C.contentWindow]).on("touchstart mousedown.xdsoft",function t(){j.trigger("close.xdsoft"),e([C.ownerDocument.body,C.contentWindow]).off("touchstart mousedown.xdsoft",t)}))}).on("close.xdsoft",function(t){var a=!0;I.find(".xdsoft_month,.xdsoft_year").find(".xdsoft_select").hide(),C.onClose&&e.isFunction(C.onClose)&&(a=C.onClose.call(j,A.currentTime,j.data("input"),t)),!1===a||C.opened||C.inline||j.hide(),t.stopPropagation()}).on("toggle.xdsoft",function(){j.is(":visible")?j.trigger("close.xdsoft"):j.trigger("open.xdsoft")}).data("input",a),q=0,j.data("xdsoft_datetime",A),j.setOptions(C),A.setCurrentTime(i()),a.data("xdsoft_datetimepicker",j).on("open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart",function(){a.is(":disabled")||a.data("xdsoft_datetimepicker").is(":visible")&&C.closeOnInputClick||C.openOnFocus&&(clearTimeout(q),q=setTimeout(function(){a.is(":disabled")||(U=!0,A.setCurrentTime(i(),!0),C.mask&&s(C),j.trigger("open.xdsoft"))},100))}).on("keydown.xdsoft",function(t){var a,r=t.which;return-1!==[p].indexOf(r)&&C.enterLikeTab?(a=e("input:visible,textarea:visible,button:visible,a:visible"),j.trigger("close.xdsoft"),a.eq(a.index(this)+1).focus(),!1):-1!==[T].indexOf(r)?(j.trigger("close.xdsoft"),!0):void 0}).on("blur.xdsoft",function(){j.trigger("close.xdsoft")})},u=function(t){var a=t.data("xdsoft_datetimepicker");a&&(a.data("xdsoft_datetime",null),a.remove(),t.data("xdsoft_datetimepicker",null).off(".xdsoft"),e(C.contentWindow).off("resize.xdsoft"),e([C.contentWindow,C.ownerDocument.body]).off("mousedown.xdsoft touchstart"),t.unmousewheel&&t.unmousewheel())},e(C.ownerDocument).off("keydown.xdsoftctrl keyup.xdsoftctrl").on("keydown.xdsoftctrl",function(e){e.keyCode===h&&(F=!0)}).on("keyup.xdsoftctrl",function(e){e.keyCode===h&&(F=!1)}),this.each(function(){var t=e(this).data("xdsoft_datetimepicker");if(t){if("string"===e.type(n))switch(n){case"show":e(this).select().focus(),t.trigger("open.xdsoft");break;case"hide":t.trigger("close.xdsoft");break;case"toggle":t.trigger("toggle.xdsoft");break;case"destroy":u(e(this));break;case"reset":this.value=this.defaultValue,this.value&&t.data("xdsoft_datetime").isValidDate(r.parseDate(this.value,C.format))||t.data("changed",!1),t.data("xdsoft_datetime").setCurrentTime(this.value);break;case"validate":t.data("input").trigger("blur.xdsoft");break;default:t[n]&&e.isFunction(t[n])&&(d=t[n](i))}else t.setOptions(n);return 0}"string"!==e.type(n)&&(!C.lazyInit||C.open||C.inline?s(e(this)):Y(e(this)))}),d},e.fn.datetimepicker.defaults=a};!function(e){"function"==typeof define&&define.amd?define(["jquery","jquery-mousewheel"],e):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(datetimepickerFactory),function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(e){function t(t){var i=t||window.event,s=u.call(arguments,1),d=0,f=0,c=0,m=0,h=0,g=0;if(t=e.event.fix(i),t.type="mousewheel","detail"in i&&(c=-1*i.detail),"wheelDelta"in i&&(c=i.wheelDelta),"wheelDeltaY"in i&&(c=i.wheelDeltaY),"wheelDeltaX"in i&&(f=-1*i.wheelDeltaX),"axis"in i&&i.axis===i.HORIZONTAL_AXIS&&(f=-1*c,c=0),d=0===c?f:c,"deltaY"in i&&(d=c=-1*i.deltaY),"deltaX"in i&&(f=i.deltaX,0===c&&(d=-1*f)),0!==c||0!==f){if(1===i.deltaMode){var p=e.data(this,"mousewheel-line-height");d*=p,c*=p,f*=p}else if(2===i.deltaMode){var D=e.data(this,"mousewheel-page-height");d*=D,c*=D,f*=D}if(m=Math.max(Math.abs(c),Math.abs(f)),(!o||m=1?"floor":"ceil"](d/o),f=Math[f>=1?"floor":"ceil"](f/o),c=Math[c>=1?"floor":"ceil"](c/o),l.settings.normalizeOffset&&this.getBoundingClientRect){var v=this.getBoundingClientRect();h=t.clientX-v.left,g=t.clientY-v.top}return t.deltaX=f,t.deltaY=c,t.deltaFactor=o,t.offsetX=h,t.offsetY=g,t.deltaMode=0,s.unshift(t,d,f,c),n&&clearTimeout(n),n=setTimeout(a,200),(e.event.dispatch||e.event.handle).apply(this,s)}}function a(){o=null}function r(e,t){return l.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}var n,o,i=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],s="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],u=Array.prototype.slice;if(e.event.fixHooks)for(var d=i.length;d;)e.event.fixHooks[i[--d]]=e.event.mouseHooks;var l=e.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var a=s.length;a;)this.addEventListener(s[--a],t,!1);else this.onmousewheel=t;e.data(this,"mousewheel-line-height",l.getLineHeight(this)),e.data(this,"mousewheel-page-height",l.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var a=s.length;a;)this.removeEventListener(s[--a],t,!1);else this.onmousewheel=null;e.removeData(this,"mousewheel-line-height"),e.removeData(this,"mousewheel-page-height")},getLineHeight:function(t){var a=e(t),r=a["offsetParent"in e.fn?"offsetParent":"parent"]();return r.length||(r=e("body")),parseInt(r.css("fontSize"),10)||parseInt(a.css("fontSize"),10)||16},getPageHeight:function(t){return e(t).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};e.fn.extend({mousewheel:function(e){return e?this.bind("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.unbind("mousewheel",e)}})}); \ No newline at end of file diff --git a/resources/datetime-picker/datetimepicker.min.css b/resources/datetime-picker/datetimepicker.min.css new file mode 100644 index 0000000..14a08a1 --- /dev/null +++ b/resources/datetime-picker/datetimepicker.min.css @@ -0,0 +1 @@ +.xdsoft_datetimepicker{box-shadow:0 5px 15px -5px rgba(0,0,0,0.506);background:#fff;border-bottom:1px solid #bbb;border-left:1px solid #ccc;border-right:1px solid #ccc;border-top:1px solid #ccc;color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:8px;padding-left:0;padding-top:2px;position:absolute;z-index:9999;-moz-box-sizing:border-box;box-sizing:border-box;display:none}.xdsoft_datetimepicker.xdsoft_rtl{padding:8px 0 8px 8px}.xdsoft_datetimepicker iframe{position:absolute;left:0;top:0;width:75px;height:210px;background:transparent;border:0}.xdsoft_datetimepicker button{border:none !important}.xdsoft_noselect{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.xdsoft_noselect::selection{background:transparent}.xdsoft_noselect::-moz-selection{background:transparent}.xdsoft_datetimepicker.xdsoft_inline{display:inline-block;position:static;box-shadow:none}.xdsoft_datetimepicker *{-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin:0}.xdsoft_datetimepicker .xdsoft_datepicker,.xdsoft_datetimepicker .xdsoft_timepicker{display:none}.xdsoft_datetimepicker .xdsoft_datepicker.active,.xdsoft_datetimepicker .xdsoft_timepicker.active{display:block}.xdsoft_datetimepicker .xdsoft_datepicker{width:224px;float:left;margin-left:8px}.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_datepicker{float:right;margin-right:8px;margin-left:0}.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_datepicker{width:256px}.xdsoft_datetimepicker .xdsoft_timepicker{width:58px;float:left;text-align:center;margin-left:8px;margin-top:0}.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker{float:right;margin-right:8px;margin-left:0}.xdsoft_datetimepicker .xdsoft_datepicker.active+.xdsoft_timepicker{margin-top:8px;margin-bottom:3px}.xdsoft_datetimepicker .xdsoft_monthpicker{position:relative;text-align:center}.xdsoft_datetimepicker .xdsoft_label i,.xdsoft_datetimepicker .xdsoft_prev,.xdsoft_datetimepicker .xdsoft_next,.xdsoft_datetimepicker .xdsoft_today_button{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Q0NBRjI1NjM0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Q0NBRjI1NjQ0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDQ0FGMjU2MTQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDQ0FGMjU2MjQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoNEP54AAAIOSURBVHja7Jq9TsMwEMcxrZD4WpBYeKUCe+kTMCACHZh4BFfHO/AAIHZGFhYkBBsSEqxsLCAgXKhbXYOTxh9pfJVP+qutnZ5s/5Lz2Y5I03QhWji2GIcgAokWgfCxNvcOCCGKqiSqhUp0laHOne05vdEyGMfkdxJDVjgwDlEQgYQBgx+ULJaWSXXS6r/ER5FBVR8VfGftTKcITNs+a1XpcFoExREIDF14AVIFxgQUS+h520cdud6wNkC0UBw6BCO/HoCYwBhD8QCkQ/x1mwDyD4plh4D6DDV0TAGyo4HcawLIBBSLDkHeH0Mg2yVP3l4TQMZQDDsEOl/MgHQqhMNuE0D+oBh0CIr8MAKyazBH9WyBuKxDWgbXfjNf32TZ1KWm/Ap1oSk/R53UtQ5xTh3LUlMmT8gt6g51Q9p+SobxgJQ/qmsfZhWywGFSl0yBjCLJCMgXail3b7+rumdVJ2YRss4cN+r6qAHDkPWjPjdJCF4n9RmAD/V9A/Wp4NQassDjwlB6XBiCxcJQWmZZb8THFilfy/lfrTvLghq2TqTHrRMTKNJ0sIhdo15RT+RpyWwFdY96UZ/LdQKBGjcXpcc1AlSFEfLmouD+1knuxBDUVrvOBmoOC/rEcN7OQxKVeJTCiAdUzUJhA2Oez9QTkp72OTVcxDcXY8iKNkxGAJXmJCOQwOa6dhyXsOa6XwEGAKdeb5ET3rQdAAAAAElFTkSuQmCC)}.xdsoft_datetimepicker .xdsoft_label i{opacity:.5;background-position:-92px -19px;display:inline-block;width:9px;height:20px;vertical-align:middle}.xdsoft_datetimepicker .xdsoft_prev{float:left;background-position:-20px 0}.xdsoft_datetimepicker .xdsoft_today_button{float:left;background-position:-70px 0;margin-left:5px}.xdsoft_datetimepicker .xdsoft_next{float:right;background-position:0 0}.xdsoft_datetimepicker .xdsoft_next,.xdsoft_datetimepicker .xdsoft_prev,.xdsoft_datetimepicker .xdsoft_today_button{background-color:transparent;background-repeat:no-repeat;border:0 none;cursor:pointer;display:block;height:30px;opacity:.5;-ms-filter:"alpha(opacity=50)";outline:medium none;overflow:hidden;padding:0;position:relative;text-indent:100%;white-space:nowrap;width:20px;min-width:0}.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev,.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_next{float:none;background-position:-40px -15px;height:15px;width:30px;display:block;margin-left:14px;margin-top:7px}.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker .xdsoft_prev,.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker .xdsoft_next{float:none;margin-left:0;margin-right:14px}.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev{background-position:-40px 0;margin-bottom:7px;margin-top:0}.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box{height:151px;overflow:hidden;border-bottom:1px solid #ddd}.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div{background:#f5f5f5;border-top:1px solid #ddd;color:#666;font-size:12px;text-align:center;border-collapse:collapse;cursor:pointer;border-bottom-width:0;height:25px;line-height:25px}.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div:first-child{border-top-width:0}.xdsoft_datetimepicker .xdsoft_today_button:hover,.xdsoft_datetimepicker .xdsoft_next:hover,.xdsoft_datetimepicker .xdsoft_prev:hover{opacity:1;-ms-filter:"alpha(opacity=100)"}.xdsoft_datetimepicker .xdsoft_label{display:inline;position:relative;z-index:9999;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:bold;background-color:#fff;float:left;width:182px;text-align:center;cursor:pointer}.xdsoft_datetimepicker .xdsoft_label:hover>span{text-decoration:underline}.xdsoft_datetimepicker .xdsoft_label:hover i{opacity:1.0}.xdsoft_datetimepicker .xdsoft_label>.xdsoft_select{border:1px solid #ccc;position:absolute;right:0;top:30px;z-index:101;display:none;background:#fff;max-height:160px;overflow-y:hidden}.xdsoft_datetimepicker .xdsoft_label>.xdsoft_select.xdsoft_monthselect{right:-7px}.xdsoft_datetimepicker .xdsoft_label>.xdsoft_select.xdsoft_yearselect{right:2px}.xdsoft_datetimepicker .xdsoft_label>.xdsoft_select>div>.xdsoft_option:hover{color:#fff;background:#ff8000}.xdsoft_datetimepicker .xdsoft_label>.xdsoft_select>div>.xdsoft_option{padding:2px 10px 2px 5px;text-decoration:none !important}.xdsoft_datetimepicker .xdsoft_label>.xdsoft_select>div>.xdsoft_option.xdsoft_current{background:#3af;box-shadow:#178fe5 0 1px 3px 0 inset;color:#fff;font-weight:700}.xdsoft_datetimepicker .xdsoft_month{width:100px;text-align:right}.xdsoft_datetimepicker .xdsoft_calendar{clear:both}.xdsoft_datetimepicker .xdsoft_year{width:48px;margin-left:5px}.xdsoft_datetimepicker .xdsoft_calendar table{border-collapse:collapse;width:100%}.xdsoft_datetimepicker .xdsoft_calendar td>div{padding-right:5px}.xdsoft_datetimepicker .xdsoft_calendar th{height:25px}.xdsoft_datetimepicker .xdsoft_calendar td,.xdsoft_datetimepicker .xdsoft_calendar th{width:14.2857142%;background:#f5f5f5;border:1px solid #ddd;color:#666;font-size:12px;text-align:right;vertical-align:middle;padding:0;border-collapse:collapse;cursor:pointer;height:25px}.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar td,.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar th{width:12.5%}.xdsoft_datetimepicker .xdsoft_calendar th{background:#f1f1f1}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_today{color:#3af}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_default{background:#ffe9d2;box-shadow:#ffb871 0 1px 4px 0 inset;color:#000}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_mint{background:#c1ffc9;box-shadow:#00dd1c 0 1px 4px 0 inset;color:#000}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_default,.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current,.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_current{background:#3af;box-shadow:#178fe5 0 1px 3px 0 inset;color:#fff;font-weight:700}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month,.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled,.xdsoft_datetimepicker .xdsoft_time_box>div>div.xdsoft_disabled{opacity:.5;-ms-filter:"alpha(opacity=50)";cursor:default}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month.xdsoft_disabled{opacity:.2;-ms-filter:"alpha(opacity=20)"}.xdsoft_datetimepicker .xdsoft_calendar td:hover,.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div:hover{color:#fff !important;background:#ff8000 !important;box-shadow:none !important}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current.xdsoft_disabled:hover,.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_current.xdsoft_disabled:hover{background:#3af !important;box-shadow:#178fe5 0 1px 3px 0 inset !important;color:#fff !important}.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled:hover,.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_disabled:hover{color:inherit !important;background:inherit !important;box-shadow:inherit !important}.xdsoft_datetimepicker .xdsoft_calendar th{font-weight:700;text-align:center;color:#999;cursor:default}.xdsoft_datetimepicker .xdsoft_copyright{color:#ccc !important;font-size:10px;clear:both;float:none;margin-left:8px}.xdsoft_datetimepicker .xdsoft_copyright a{color:#eee !important}.xdsoft_datetimepicker .xdsoft_copyright a:hover{color:#aaa !important}.xdsoft_time_box{position:relative;border:1px solid #ccc}.xdsoft_scrollbar>.xdsoft_scroller{background:#ccc !important;height:20px;border-radius:3px}.xdsoft_scrollbar{position:absolute;width:7px;right:0;top:0;bottom:0;cursor:pointer}.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_scrollbar{left:0;right:auto}.xdsoft_scroller_box{position:relative}.xdsoft_datetimepicker.xdsoft_dark{box-shadow:0 5px 15px -5px rgba(255,255,255,0.506);background:#000;border-bottom:1px solid #444;border-left:1px solid #333;border-right:1px solid #333;border-top:1px solid #333;color:#ccc}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box{border-bottom:1px solid #222}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box>div>div{background:#0a0a0a;border-top:1px solid #222;color:#999}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label{background-color:#000}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label>.xdsoft_select{border:1px solid #333;background:#000}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label>.xdsoft_select>div>.xdsoft_option:hover{color:#000;background:#007fff}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label>.xdsoft_select>div>.xdsoft_option.xdsoft_current{background:#c50;box-shadow:#b03e00 0 1px 3px 0 inset;color:#000}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label i,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_prev,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_next,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_today_button{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUExQUUzOTA0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUExQUUzOTE0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQTFBRTM4RTQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQTFBRTM4RjQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pp0VxGEAAAIASURBVHja7JrNSgMxEMebtgh+3MSLr1T1Xn2CHoSKB08+QmR8Bx9A8e7RixdB9CKCoNdexIugxFlJa7rNZneTbLIpM/CnNLsdMvNjM8l0mRCiQ9Ye61IKCAgZAUnH+mU3MMZaHYChBnJUDzWOFZdVfc5+ZFLbrWDeXPwbxIqrLLfaeS0hEBVGIRQCEiZoHQwtlGSByCCdYBl8g8egTTAWoKQMRBRBcZxYlhzhKegqMOageErsCHVkk3hXIFooDgHB1KkHIHVgzKB4ADJQ/A1jAFmAYhkQqA5TOBtocrKrgXwQA8gcFIuAIO8sQSA7hidvPwaQGZSaAYHOUWJABhWWw2EMIH9QagQERU4SArJXo0ZZL18uvaxejXt/Em8xjVBXmvFr1KVm/AJ10tRe2XnraNqaJvKE3KHuUbfK1E+VHB0q40/y3sdQSxY4FHWeKJCunP8UyDdqJZenT3ntVV5jIYCAh20vT7ioP8tpf6E2lfEMwERe+whV1MHjwZB7PBiCxcGQWwKZKD62lfGNnP/1poFAA60T7rF1UgcKd2id3KDeUS+oLWV8DfWAepOfq00CgQabi9zjcgJVYVD7PVzQUAUGAQkbNJTBICDhgwYTjDYD6XeW08ZKh+A4pYkzenOxXUbvZcWz7E8ykRMnIHGX1XPl+1m2vPYpL+2qdb8CDAARlKFEz/ZVkAAAAABJRU5ErkJggg==)}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th{background:#0a0a0a;border:1px solid #222;color:#999}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th{background:#0e0e0e}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_today{color:#c50}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_default{background:#ffe9d2;box-shadow:#ffb871 0 1px 4px 0 inset;color:#000}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_mint{background:#c1ffc9;box-shadow:#00dd1c 0 1px 4px 0 inset;color:#000}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_default,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_current,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_current{background:#c50;box-shadow:#b03e00 0 1px 3px 0 inset;color:#000}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td:hover,.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box>div>div:hover{color:#000 !important;background:#007fff !important}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th{color:#666}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright{color:#333 !important}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a{color:#111 !important}.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a:hover{color:#555 !important}.xdsoft_dark .xdsoft_time_box{border:1px solid #333}.xdsoft_dark .xdsoft_scrollbar>.xdsoft_scroller{background:#333 !important}.xdsoft_datetimepicker .xdsoft_save_selected{display:block;border:1px solid #ddd !important;margin-top:5px;width:100%;color:#454551;font-size:13px}.xdsoft_datetimepicker .blue-gradient-button{font-family:"museo-sans","Book Antiqua",sans-serif;font-size:12px;font-weight:300;color:#82878c;height:28px;position:relative;padding:4px 17px 4px 33px;border:1px solid #d7d8da;background:-moz-linear-gradient(top,#fff 0,#f4f8fa 73%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#fff),color-stop(73%,#f4f8fa));background:-webkit-linear-gradient(top,#fff 0,#f4f8fa 73%);background:-o-linear-gradient(top,#fff 0,#f4f8fa 73%);background:-ms-linear-gradient(top,#fff 0,#f4f8fa 73%);background:linear-gradient(to bottom,#fff 0,#f4f8fa 73%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff',endColorstr='#f4f8fa',GradientType=0)}.xdsoft_datetimepicker .blue-gradient-button:hover,.xdsoft_datetimepicker .blue-gradient-button:focus,.xdsoft_datetimepicker .blue-gradient-button:hover span,.xdsoft_datetimepicker .blue-gradient-button:focus span{color:#454551;background:-moz-linear-gradient(top,#f4f8fa 0,#FFF 73%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#f4f8fa),color-stop(73%,#FFF));background:-webkit-linear-gradient(top,#f4f8fa 0,#FFF 73%);background:-o-linear-gradient(top,#f4f8fa 0,#FFF 73%);background:-ms-linear-gradient(top,#f4f8fa 0,#FFF 73%);background:linear-gradient(to bottom,#f4f8fa 0,#FFF 73%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f4f8fa',endColorstr='#FFF',GradientType=0)} \ No newline at end of file From 97d02399630d8feacbd57695539ce18141214bc0 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 17:34:33 -0500 Subject: [PATCH 07/42] Add duplication check for contest edit in group --- judge/forms.py | 30 +++++++++++++++++++++++- judge/views/organization.py | 8 +++++-- locale/vi/LC_MESSAGES/django.po | 29 ++++++++++++++--------- resources/widgets.scss | 14 ++++++----- templates/organization/contest/edit.html | 5 ++-- templates/organization/form.html | 3 +-- 6 files changed, 64 insertions(+), 25 deletions(-) diff --git a/judge/forms.py b/judge/forms.py index 5bc43a9..32fc75e 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -538,9 +538,37 @@ class ContestProblemForm(ModelForm): } +class ContestProblemModelFormSet(BaseModelFormSet): + def is_valid(self): + valid = super().is_valid() + + if not valid: + return valid + + problems = set() + duplicates = [] + + for form in self.forms: + if form.cleaned_data and not form.cleaned_data.get("DELETE", False): + problem = form.cleaned_data.get("problem") + if problem in problems: + duplicates.append(problem) + else: + problems.add(problem) + + if duplicates: + for form in self.forms: + problem = form.cleaned_data.get("problem") + if problem in duplicates: + form.add_error("problem", _("This problem is duplicated.")) + return False + + return True + + class ContestProblemFormSet( formset_factory( - ContestProblemForm, formset=BaseModelFormSet, extra=6, can_delete=True + ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True ) ): model = ContestProblem diff --git a/judge/views/organization.py b/judge/views/organization.py index 962be71..0576afe 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -926,8 +926,12 @@ class EditOrganizationContest( super().post(request, *args, **kwargs) return HttpResponseRedirect( reverse( - "organization_contests", - args=(self.organization_id, self.organization.slug), + "organization_contest_edit", + args=( + self.organization_id, + self.organization.slug, + self.contest.key, + ), ) ) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index d1e8a78..0cb9282 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-26 03:42+0700\n" +"POT-Creation-Date: 2023-08-26 05:31+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -555,6 +555,10 @@ msgstr "" msgid "You don't have permission in this group." msgstr "Bạn không có quyền chấm lại bài." +#: judge/forms.py:563 +msgid "This problem is duplicated." +msgstr "Bài này bị lặp" + #: judge/jinja2/datetime.py:26 templates/blog/blog.html:28 #: templates/blog/dashboard.html:21 msgid "N j, Y, g:i a" @@ -2883,19 +2887,19 @@ msgstr "Thay đổi Email" msgid "Change Email" msgstr "Thay đổi Email" -#: judge/views/email.py:65 templates/user/edit-profile.html:120 +#: judge/views/email.py:71 templates/user/edit-profile.html:120 msgid "Change email" msgstr "Thay đổi email" -#: judge/views/email.py:89 +#: judge/views/email.py:94 msgid "Success" msgstr "Thành công" -#: judge/views/email.py:93 +#: judge/views/email.py:98 msgid "Invalid" msgstr "Không hợp lệ" -#: judge/views/email.py:102 +#: judge/views/email.py:107 msgid "Email change pending" msgstr "Yêu cầu thay đổi email đang đợi xác thực." @@ -4426,8 +4430,8 @@ msgstr "Hoạt động" #: templates/organization/blog/edit.html:36 #: templates/organization/contest/add.html:36 -#: templates/organization/contest/edit.html:87 -#: templates/organization/form.html:24 +#: templates/organization/contest/edit.html:86 +#: templates/organization/form.html:23 msgid "Save" msgstr "Lưu" @@ -4441,12 +4445,15 @@ msgid "Author" msgstr "Tác giả" #: templates/organization/blog/pending.html:14 -#, fuzzy -#| msgid "posted time" msgid "Post time" -msgstr "thời gian đăng" +msgstr "Thời gian đăng" -#: templates/organization/contest/edit.html:61 +#: templates/organization/contest/edit.html:40 +#: templates/organization/form.html:6 +msgid "Please fix below errors" +msgstr "Vui lòng sửa các lỗi bên dưới" + +#: templates/organization/contest/edit.html:60 msgid "If you run out of rows, click Save" msgstr "Ấn nút lưu lại nếu cần thêm hàng" diff --git a/resources/widgets.scss b/resources/widgets.scss index f33882d..085d508 100644 --- a/resources/widgets.scss +++ b/resources/widgets.scss @@ -606,6 +606,14 @@ ul.select2-selection__rendered { background: gray; } +ul.errorlist { + margin: 0px; + text-align: right; + list-style: none; + padding: 0px; + color: red; +} + .registration-form { .sortedm2m-container, .sortedm2m-container p.selector-filter { width: 300px; @@ -682,12 +690,6 @@ ul.select2-selection__rendered { width: 450px; } - ul.errorlist { - margin: 0px; - text-align: right; - list-style: none; - } - .full-textfield { padding-top: 0.5em; } diff --git a/templates/organization/contest/edit.html b/templates/organization/contest/edit.html index 101bfbf..5682504 100644 --- a/templates/organization/contest/edit.html +++ b/templates/organization/contest/edit.html @@ -34,11 +34,10 @@ {% block middle_content %}
{% csrf_token %} - {% if form.errors %} + {% if form.errors or problems_form.errors %}
x - {{ form.non_field_errors() }} - {{ form.errors }} + {{_("Please fix below errors")}}
{% endif %} {% for field in form %} diff --git a/templates/organization/form.html b/templates/organization/form.html index 5201ace..85c86d9 100644 --- a/templates/organization/form.html +++ b/templates/organization/form.html @@ -3,8 +3,7 @@ {% if form.errors %}
x - {{ form.non_field_errors() }} - {{ form.errors }} + {{ _("Please fix below errors") }}
{% endif %} {% for field in form %} From 8f046c59c172ede5c4d2a9f2540a7e5989ad0f8a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 18:12:53 -0500 Subject: [PATCH 08/42] Drop output_prefix_override and use show_testcases --- judge/admin/contest.py | 3 +- judge/forms.py | 2 +- judge/migrations/0164_show_testcase.py | 30 +++++++++++++++++++ .../0165_drop_output_prefix_override.py | 17 +++++++++++ judge/models/contest.py | 7 ++--- judge/models/profile.py | 7 +++++ judge/views/submission.py | 6 ++-- templates/submission/status-testcases.html | 5 ---- 8 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 judge/migrations/0164_show_testcase.py create mode 100644 judge/migrations/0165_drop_output_prefix_override.py diff --git a/judge/admin/contest.py b/judge/admin/contest.py index f183880..9c8b6fd 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -71,7 +71,6 @@ class ContestProblemInlineForm(ModelForm): "hidden_subtasks": TextInput(attrs={"size": "3"}), "points": TextInput(attrs={"size": "1"}), "order": TextInput(attrs={"size": "1"}), - "output_prefix_override": TextInput(attrs={"size": "1"}), } @@ -86,7 +85,7 @@ class ContestProblemInline(admin.TabularInline): "is_pretested", "max_submissions", "hidden_subtasks", - "output_prefix_override", + "show_testcases", "order", "rejudge_column", ) diff --git a/judge/forms.py b/judge/forms.py index 32fc75e..d71ffbc 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -528,7 +528,7 @@ class ContestProblemForm(ModelForm): "problem", "points", "partial", - "output_prefix_override", + "show_testcases", "max_submissions", ) widgets = { diff --git a/judge/migrations/0164_show_testcase.py b/judge/migrations/0164_show_testcase.py new file mode 100644 index 0000000..33cbdf8 --- /dev/null +++ b/judge/migrations/0164_show_testcase.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.18 on 2023-08-25 23:03 + +from django.db import migrations, models + + +def migrate_show_testcases(apps, schema_editor): + ContestProblem = apps.get_model("judge", "ContestProblem") + + for c in ContestProblem.objects.all(): + if c.output_prefix_override == 1: + c.show_testcases = True + c.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0163_email_change"), + ] + + operations = [ + migrations.AddField( + model_name="contestproblem", + name="show_testcases", + field=models.BooleanField(default=False, verbose_name="visible testcases"), + ), + migrations.RunPython( + migrate_show_testcases, migrations.RunPython.noop, atomic=True + ), + ] diff --git a/judge/migrations/0165_drop_output_prefix_override.py b/judge/migrations/0165_drop_output_prefix_override.py new file mode 100644 index 0000000..ea9b17a --- /dev/null +++ b/judge/migrations/0165_drop_output_prefix_override.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.18 on 2023-08-25 23:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0164_show_testcase"), + ] + + operations = [ + migrations.RemoveField( + model_name="contestproblem", + name="output_prefix_override", + ), + ] diff --git a/judge/models/contest.py b/judge/models/contest.py index 1129076..8bb3d5b 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -772,12 +772,9 @@ class ContestProblem(models.Model): partial = models.BooleanField(default=True, verbose_name=_("partial")) is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested")) order = models.PositiveIntegerField(db_index=True, verbose_name=_("order")) - output_prefix_override = models.IntegerField( - help_text=_("0 to not show testcases, 1 to show"), + show_testcases = models.BooleanField( verbose_name=_("visible testcases"), - null=True, - blank=True, - default=0, + default=False, ) max_submissions = models.IntegerField( help_text=_( diff --git a/judge/models/profile.py b/judge/models/profile.py index eb3572a..ccb0e87 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -109,6 +109,13 @@ class Organization(models.Model): "Organization membership test must be Profile or primany key" ) + def delete(self, *args, **kwargs): + contests = self.contest_set + for contest in contests.all(): + if contest.organizations.count() == 1: + contest.delete() + super().delete(*args, **kwargs) + def __str__(self): return self.name diff --git a/judge/views/submission.py b/judge/views/submission.py index 147a2cc..1556dd9 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -259,13 +259,13 @@ class SubmissionStatus(SubmissionDetailBase): ) contest = submission.contest_or_none - prefix_length = 0 + show_testcases = False can_see_testcases = self.access_testcases_in_contest() if contest is not None: - prefix_length = contest.problem.output_prefix_override or 0 + show_testcases = contest.problem.show_testcases or False - if contest is None or prefix_length > 0 or can_see_testcases: + if contest is None or show_testcases or can_see_testcases: context["cases_data"] = get_cases_data(submission) context["can_see_testcases"] = True try: diff --git a/templates/submission/status-testcases.html b/templates/submission/status-testcases.html index 4d4b435..2d2547d 100644 --- a/templates/submission/status-testcases.html +++ b/templates/submission/status-testcases.html @@ -1,8 +1,3 @@ -{% if submission.contest_or_none %} - {% set prefix_length = submission.contest_or_none.problem.output_prefix_override %} -{% else %} - {% set prefix_length = None %} -{% endif %} {% set is_pretest = submission.is_pretested %} {% if submission.status != 'IE' %} From 9a825225dd6c622b4c21dfe2d3fa3e7ef2d2dfa4 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 18:38:19 -0500 Subject: [PATCH 09/42] Small UI improvement --- judge/forms.py | 2 +- templates/organization/contest/edit.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/judge/forms.py b/judge/forms.py index d71ffbc..fc73b31 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -533,7 +533,7 @@ class ContestProblemForm(ModelForm): ) widgets = { "problem": HeavySelect2Widget( - data_view="problem_select2", attrs={"style": "width:100%"} + data_view="problem_select2", attrs={"style": "width: 100%"} ), } diff --git a/templates/organization/contest/edit.html b/templates/organization/contest/edit.html index 5682504..d5d9228 100644 --- a/templates/organization/contest/edit.html +++ b/templates/organization/contest/edit.html @@ -17,7 +17,7 @@ display: inline-flex; } .problems-problem { - width: 40%; + max-width: 60vh; } input[type=number] { width: 5em; @@ -63,7 +63,7 @@ {% for field in problems_form[0] %} {% if not field.is_hidden %} - + {{field.label}} {% endif %} From 3ff608e4ffb1220c4866e7a94a649b61bc6f06b3 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 25 Aug 2023 23:50:03 -0500 Subject: [PATCH 10/42] Add password to email change form --- judge/views/email.py | 14 +++++++++++++- templates/email_change/email_change.html | 4 ---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/judge/views/email.py b/judge/views/email.py index 56c43b9..5544daf 100644 --- a/judge/views/email.py +++ b/judge/views/email.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _ from django.urls import reverse from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User +from django.contrib.auth.hashers import check_password from urllib.parse import urlencode, urlunparse, urlparse @@ -18,6 +19,11 @@ from judge.utils.email_render import render_email_message class EmailChangeForm(forms.Form): new_email = forms.EmailField(label=_("New Email")) + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) def clean_new_email(self): new_email = self.cleaned_data.get("new_email") @@ -25,10 +31,16 @@ class EmailChangeForm(forms.Form): raise forms.ValidationError(_("An account with this email already exists.")) return new_email + def clean_password(self): + password = self.cleaned_data.get("password") + if not self.user.check_password(password): + raise forms.ValidationError("Invalid password") + return password + @login_required def email_change_view(request): - form = EmailChangeForm(request.POST or None) + form = EmailChangeForm(request.POST or None, user=request.user) if request.method == "POST" and form.is_valid(): new_email = request.POST.get("new_email") diff --git a/templates/email_change/email_change.html b/templates/email_change/email_change.html index 6033d18..edfbfee 100644 --- a/templates/email_change/email_change.html +++ b/templates/email_change/email_change.html @@ -3,10 +3,6 @@ {% block media %} From f11d9b4b53cb13a651e797532e88f5c92b111b31 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 26 Aug 2023 12:38:50 -0500 Subject: [PATCH 11/42] Fix ticket ui on mobile --- resources/ticket.scss | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/resources/ticket.scss b/resources/ticket.scss index d7f9143..092c1a1 100644 --- a/resources/ticket.scss +++ b/resources/ticket.scss @@ -150,14 +150,6 @@ padding: 7px; } - .message .content :first-child { - margin-top: 0; - } - - .message .content :last-child { - margin-bottom: 0; - } - .new-message .detail { padding: 8px 10px; } @@ -174,4 +166,10 @@ padding-left: 0.5em; padding-top: 1.65em; } +} + +@media (max-width: 799px) { + .ticket-container { + flex-direction: column-reverse; + } } \ No newline at end of file From 2854ac97e930ed112d5ac126409b4a17db032714 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 28 Aug 2023 14:20:35 -0500 Subject: [PATCH 12/42] Make chat faster --- .../migrations/0013_alter_message_time.py | 20 + .../migrations/0014_userroom_unread_count.py | 38 ++ chat_box/models.py | 26 +- chat_box/utils.py | 35 +- chat_box/views.py | 176 +++--- dmoj/settings.py | 3 + judge/migrations/0166_display_rank_index.py | 28 + judge/models/profile.py | 1 + templates/chat/chat.html | 562 +----------------- templates/chat/chat_css.html | 262 ++++---- templates/chat/chat_js.html | 553 +++++++++++++++++ 11 files changed, 903 insertions(+), 801 deletions(-) create mode 100644 chat_box/migrations/0013_alter_message_time.py create mode 100644 chat_box/migrations/0014_userroom_unread_count.py create mode 100644 judge/migrations/0166_display_rank_index.py create mode 100644 templates/chat/chat_js.html diff --git a/chat_box/migrations/0013_alter_message_time.py b/chat_box/migrations/0013_alter_message_time.py new file mode 100644 index 0000000..0f4ddc3 --- /dev/null +++ b/chat_box/migrations/0013_alter_message_time.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.18 on 2023-08-28 01:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_box", "0012_auto_20230308_1417"), + ] + + operations = [ + migrations.AlterField( + model_name="message", + name="time", + field=models.DateTimeField( + auto_now_add=True, db_index=True, verbose_name="posted time" + ), + ), + ] diff --git a/chat_box/migrations/0014_userroom_unread_count.py b/chat_box/migrations/0014_userroom_unread_count.py new file mode 100644 index 0000000..8f407b3 --- /dev/null +++ b/chat_box/migrations/0014_userroom_unread_count.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.18 on 2023-08-28 06:02 + +from django.db import migrations, models + + +def migrate(apps, schema_editor): + UserRoom = apps.get_model("chat_box", "UserRoom") + Message = apps.get_model("chat_box", "Message") + + for ur in UserRoom.objects.all(): + if not ur.room: + continue + messages = ur.room.message_set + last_msg = messages.first() + try: + if last_msg and last_msg.author != ur.user: + ur.unread_count = messages.filter(time__gte=ur.last_seen).count() + else: + ur.unread_count = 0 + ur.save() + except: + continue + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_box", "0013_alter_message_time"), + ] + + operations = [ + migrations.AddField( + model_name="userroom", + name="unread_count", + field=models.IntegerField(db_index=True, default=0), + ), + migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True), + ] diff --git a/chat_box/models.py b/chat_box/models.py index 61e39a6..b6d7220 100644 --- a/chat_box/models.py +++ b/chat_box/models.py @@ -1,9 +1,10 @@ from django.db import models -from django.db.models import CASCADE +from django.db.models import CASCADE, Q from django.utils.translation import gettext_lazy as _ from judge.models.profile import Profile +from judge.caching import cache_wrapper __all__ = ["Message", "Room", "UserRoom", "Ignore"] @@ -29,7 +30,9 @@ class Room(models.Model): class Message(models.Model): author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE) - time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) + time = models.DateTimeField( + verbose_name=_("posted time"), auto_now_add=True, db_index=True + ) body = models.TextField(verbose_name=_("body of comment"), max_length=8192) hidden = models.BooleanField(verbose_name="is hidden", default=False) room = models.ForeignKey( @@ -56,6 +59,7 @@ class UserRoom(models.Model): Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True ) last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True) + unread_count = models.IntegerField(default=0, db_index=True) class Meta: unique_together = ("user", "room") @@ -74,11 +78,9 @@ class Ignore(models.Model): @classmethod def is_ignored(self, current_user, new_friend): try: - return ( - current_user.ignored_chat_users.get() - .ignored_users.filter(id=new_friend.id) - .exists() - ) + return current_user.ignored_chat_users.ignored_users.filter( + id=new_friend.id + ).exists() except: return False @@ -89,6 +91,16 @@ class Ignore(models.Model): except: return Profile.objects.none() + @classmethod + def get_ignored_rooms(self, user): + try: + ignored_users = self.objects.get(user=user).ignored_users.all() + return Room.objects.filter(Q(user_one=user) | Q(user_two=user)).filter( + Q(user_one__in=ignored_users) | Q(user_two__in=ignored_users) + ) + except: + return Room.objects.none() + @classmethod def add_ignore(self, current_user, friend): ignore, created = self.objects.get_or_create(user=current_user) diff --git a/chat_box/utils.py b/chat_box/utils.py index e25e861..dd59d98 100644 --- a/chat_box/utils.py +++ b/chat_box/utils.py @@ -1,10 +1,12 @@ from cryptography.fernet import Fernet +import hmac +import hashlib from django.conf import settings -from django.db.models import OuterRef, Count, Subquery, IntegerField +from django.db.models import OuterRef, Count, Subquery, IntegerField, Q from django.db.models.functions import Coalesce -from chat_box.models import Ignore, Message, UserRoom +from chat_box.models import Ignore, Message, UserRoom, Room secret_key = settings.CHAT_SECRET_KEY fernet = Fernet(secret_key) @@ -24,25 +26,22 @@ def decrypt_url(message_encrypted): return None, None -def get_unread_boxes(profile): - ignored_users = Ignore.get_ignored_users(profile) - - mess = ( - Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen")) - .exclude(author=profile) - .exclude(author__in=ignored_users) - .order_by() - .values("room") - .annotate(unread_count=Count("pk")) - .values("unread_count") +def encrypt_channel(channel): + return ( + hmac.new( + settings.CHAT_SECRET_KEY.encode(), + channel.encode(), + hashlib.sha512, + ).hexdigest()[:16] + + "%s" % channel ) + +def get_unread_boxes(profile): + ignored_rooms = Ignore.get_ignored_rooms(profile) unread_boxes = ( - UserRoom.objects.filter(user=profile, room__isnull=False) - .annotate( - unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0), - ) - .filter(unread_count__gte=1) + UserRoom.objects.filter(user=profile, unread_count__gt=0) + .exclude(room__in=ignored_rooms) .count() ) diff --git a/chat_box/views.py b/chat_box/views.py index cea58e1..879dfcf 100644 --- a/chat_box/views.py +++ b/chat_box/views.py @@ -21,6 +21,7 @@ from django.db.models import ( Exists, Count, IntegerField, + F, ) from django.db.models.functions import Coalesce from django.utils import timezone @@ -34,7 +35,7 @@ from judge.jinja2.gravatar import gravatar from judge.models import Friend from chat_box.models import Message, Profile, Room, UserRoom, Ignore -from chat_box.utils import encrypt_url, decrypt_url +from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel import json @@ -49,7 +50,8 @@ class ChatView(ListView): self.room_id = None self.room = None self.messages = None - self.page_size = 20 + self.first_page_size = 20 # only for first request + self.follow_up_page_size = 50 def get_queryset(self): return self.messages @@ -63,10 +65,12 @@ class ChatView(ListView): def get(self, request, *args, **kwargs): request_room = kwargs["room_id"] + page_size = self.follow_up_page_size try: last_id = int(request.GET.get("last_id")) except Exception: last_id = 1e15 + page_size = self.first_page_size only_messages = request.GET.get("only_messages") if request_room: @@ -80,11 +84,12 @@ class ChatView(ListView): request_room = None self.room_id = request_room - self.messages = Message.objects.filter( - hidden=False, room=self.room_id, id__lt=last_id - )[: self.page_size] + self.messages = ( + Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id) + .select_related("author", "author__user") + .defer("author__about", "author__user_script")[:page_size] + ) if not only_messages: - update_last_seen(request, **kwargs) return super().get(request, *args, **kwargs) return render( @@ -101,10 +106,14 @@ class ChatView(ListView): context["title"] = self.title context["last_msg"] = event.last() - context["status_sections"] = get_status_context(self.request) + context["status_sections"] = get_status_context(self.request.profile) context["room"] = self.room_id context["has_next"] = self.has_next() context["unread_count_lobby"] = get_unread_count(None, self.request.profile) + context["chat_channel"] = encrypt_channel( + "chat_" + str(self.request.profile.id) + ) + context["chat_lobby_channel"] = encrypt_channel("chat_lobby") if self.room: users_room = [self.room.user_one, self.room.user_two] users_room.remove(self.request.profile) @@ -187,7 +196,7 @@ def post_message(request): if not room: event.post( - "chat_lobby", + encrypt_channel("chat_lobby"), { "type": "lobby", "author_id": request.profile.id, @@ -199,7 +208,7 @@ def post_message(request): else: for user in room.users(): event.post( - "chat_" + str(user.id), + encrypt_channel("chat_" + str(user.id)), { "type": "private", "author_id": request.profile.id, @@ -208,6 +217,10 @@ def post_message(request): "tmp_id": request.POST.get("tmp_id"), }, ) + if user != request.profile: + UserRoom.objects.filter(user=user, room=room).update( + unread_count=F("unread_count") + 1 + ) return JsonResponse(ret) @@ -254,35 +267,33 @@ def update_last_seen(request, **kwargs): room_id = request.POST.get("room") else: return HttpResponseBadRequest() - try: profile = request.profile room = None if room_id: - room = Room.objects.get(id=int(room_id)) + room = Room.objects.filter(id=int(room_id)).first() except Room.DoesNotExist: return HttpResponseBadRequest() - except Exception as e: - return HttpResponseBadRequest() if room and not room.contain(profile): return HttpResponseBadRequest() user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room) user_room.last_seen = timezone.now() + user_room.unread_count = 0 user_room.save() return JsonResponse({"msg": "updated"}) def get_online_count(): - last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) - return Profile.objects.filter(last_access__gte=last_two_minutes).count() + last_5_minutes = timezone.now() - timezone.timedelta(minutes=5) + return Profile.objects.filter(last_access__gte=last_5_minutes).count() def get_user_online_status(user): time_diff = timezone.now() - user.last_access - is_online = time_diff <= timezone.timedelta(minutes=2) + is_online = time_diff <= timezone.timedelta(minutes=5) return is_online @@ -319,47 +330,51 @@ def user_online_status_ajax(request): ) -def get_online_status(request_user, queryset, rooms=None): - if not queryset: +def get_online_status(profile, other_profile_ids, rooms=None): + if not other_profile_ids: return None - last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) + joined_ids = ",".join([str(id) for id in other_profile_ids]) + other_profiles = Profile.objects.raw( + f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})" + ) + last_5_minutes = timezone.now() - timezone.timedelta(minutes=5) ret = [] - if rooms: - unread_count = get_unread_count(rooms, request_user) + unread_count = get_unread_count(rooms, profile) count = {} for i in unread_count: count[i["other_user"]] = i["unread_count"] - - for user in queryset: + for other_profile in other_profiles: is_online = False - if user.last_access >= last_two_minutes: + if other_profile.last_access >= last_5_minutes: is_online = True - user_dict = {"user": user, "is_online": is_online} - if rooms and user.id in count: - user_dict["unread_count"] = count[user.id] - user_dict["url"] = encrypt_url(request_user.id, user.id) + user_dict = {"user": other_profile, "is_online": is_online} + if rooms and other_profile.id in count: + user_dict["unread_count"] = count[other_profile.id] + user_dict["url"] = encrypt_url(profile.id, other_profile.id) ret.append(user_dict) return ret -def get_status_context(request, include_ignored=False): +def get_status_context(profile, include_ignored=False): if include_ignored: - ignored_users = Profile.objects.none() + ignored_users = [] queryset = Profile.objects else: - ignored_users = Ignore.get_ignored_users(request.profile) + ignored_users = list( + Ignore.get_ignored_users(profile).values_list("id", flat=True) + ) queryset = Profile.objects.exclude(id__in=ignored_users) - last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) + last_5_minutes = timezone.now() - timezone.timedelta(minutes=5) recent_profile = ( - Room.objects.filter(Q(user_one=request.profile) | Q(user_two=request.profile)) + Room.objects.filter(Q(user_one=profile) | Q(user_two=profile)) .annotate( last_msg_time=Subquery( Message.objects.filter(room=OuterRef("pk")).values("time")[:1] ), other_user=Case( - When(user_one=request.profile, then="user_two"), + When(user_one=profile, then="user_two"), default="user_one", ), ) @@ -369,50 +384,49 @@ def get_status_context(request, include_ignored=False): .values("other_user", "id")[:20] ) - recent_profile_id = [str(i["other_user"]) for i in recent_profile] - joined_id = ",".join(recent_profile_id) + recent_profile_ids = [str(i["other_user"]) for i in recent_profile] recent_rooms = [int(i["id"]) for i in recent_profile] - recent_list = None - if joined_id: - recent_list = Profile.objects.raw( - f"SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})" - ) friend_list = ( - Friend.get_friend_profiles(request.profile) - .exclude(id__in=recent_profile_id) + Friend.get_friend_profiles(profile) + .exclude(id__in=recent_profile_ids) .exclude(id__in=ignored_users) .order_by("-last_access") + .values_list("id", flat=True) ) + admin_list = ( queryset.filter(display_rank="admin") .exclude(id__in=friend_list) - .exclude(id__in=recent_profile_id) + .exclude(id__in=recent_profile_ids) + .values_list("id", flat=True) ) + all_user_status = ( - queryset.filter(display_rank="user", last_access__gte=last_two_minutes) + queryset.filter(last_access__gte=last_5_minutes) .annotate(is_online=Case(default=True, output_field=BooleanField())) .order_by("-rating") .exclude(id__in=friend_list) .exclude(id__in=admin_list) - .exclude(id__in=recent_profile_id)[:30] + .exclude(id__in=recent_profile_ids) + .values_list("id", flat=True)[:30] ) return [ { "title": "Recent", - "user_list": get_online_status(request.profile, recent_list, recent_rooms), + "user_list": get_online_status(profile, recent_profile_ids, recent_rooms), }, { "title": "Following", - "user_list": get_online_status(request.profile, friend_list), + "user_list": get_online_status(profile, friend_list), }, { "title": "Admin", - "user_list": get_online_status(request.profile, admin_list), + "user_list": get_online_status(profile, admin_list), }, { "title": "Other", - "user_list": get_online_status(request.profile, all_user_status), + "user_list": get_online_status(profile, all_user_status), }, ] @@ -423,7 +437,7 @@ def online_status_ajax(request): request, "chat/online_status.html", { - "status_sections": get_status_context(request), + "status_sections": get_status_context(request.profile), "unread_count_lobby": get_unread_count(None, request.profile), }, ) @@ -447,7 +461,6 @@ def get_or_create_room(request): return HttpResponseBadRequest() request_id, other_id = decrypt_url(decrypted_other_id) - if not other_id or not request_id or request_id != request.profile.id: return HttpResponseBadRequest() @@ -475,48 +488,31 @@ def get_or_create_room(request): def get_unread_count(rooms, user): if rooms: - mess = ( - Message.objects.filter( - room=OuterRef("room"), time__gte=OuterRef("last_seen") - ) - .exclude(author=user) - .order_by() - .values("room") - .annotate(unread_count=Count("pk")) - .values("unread_count") - ) - - return ( - UserRoom.objects.filter(user=user, room__in=rooms) - .annotate( - unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0), - other_user=Case( - When(room__user_one=user, then="room__user_two"), - default="room__user_one", - ), - ) - .filter(unread_count__gte=1) - .values("other_user", "unread_count") - ) - else: # lobby - mess = ( - Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen")) - .exclude(author=user) - .order_by() - .values("room") - .annotate(unread_count=Count("pk")) - .values("unread_count") - ) - res = ( - UserRoom.objects.filter(user=user, room__isnull=True) - .annotate( - unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0), + UserRoom.objects.filter(user=user, room__in=rooms, unread_count__gt=0) + .select_related("room__user_one", "room__user_two") + .values("unread_count", "room__user_one", "room__user_two") + ) + for ur in res: + ur["other_user"] = ( + ur["room__user_one"] + if ur["room__user_two"] == user.id + else ur["room__user_two"] ) - .values_list("unread_count", flat=True) + return res + else: # lobby + user_room = UserRoom.objects.filter(user=user, room__isnull=True).first() + if not user_room: + return 0 + last_seen = user_room.last_seen + res = ( + Message.objects.filter(room__isnull=True, time__gte=last_seen) + .exclude(author=user) + .exclude(hidden=True) + .count() ) - return res[0] if len(res) else 0 + return res @login_required diff --git a/dmoj/settings.py b/dmoj/settings.py index e205e60..d03dc27 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -484,3 +484,6 @@ except IOError: pass DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +# Chat +CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U=" diff --git a/judge/migrations/0166_display_rank_index.py b/judge/migrations/0166_display_rank_index.py new file mode 100644 index 0000000..ffea311 --- /dev/null +++ b/judge/migrations/0166_display_rank_index.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.18 on 2023-08-28 01:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0165_drop_output_prefix_override"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="display_rank", + field=models.CharField( + choices=[ + ("user", "Normal User"), + ("setter", "Problem Setter"), + ("admin", "Admin"), + ], + db_index=True, + default="user", + max_length=10, + verbose_name="display rank", + ), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index ccb0e87..3f6e430 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -183,6 +183,7 @@ class Profile(models.Model): ("setter", "Problem Setter"), ("admin", "Admin"), ), + db_index=True, ) mute = models.BooleanField( verbose_name=_("comment mute"), diff --git a/templates/chat/chat.html b/templates/chat/chat.html index 9cde791..857aaf9 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -7,21 +7,11 @@ - - + {% compress js %} + {% include "chat/chat_js.html" %} + {% endcompress %} + @@ -638,8 +89,7 @@ {% include 'chat/user_online_status.html' %}
- - + diff --git a/templates/chat/chat_css.html b/templates/chat/chat_css.html index f5bf8cd..d79a22b 100644 --- a/templates/chat/chat_css.html +++ b/templates/chat/chat_css.html @@ -1,132 +1,134 @@ - + + #content { + padding-top: 0; + } + + ::-webkit-scrollbar { + width: 20px; + } + + ::-webkit-scrollbar-track { + background-color: transparent; + } + + ::-webkit-scrollbar-thumb { + background-color: #d6dee1; + border-radius: 20px; + border: 6px solid transparent; + background-clip: content-box; + } + + ::-webkit-scrollbar-thumb:hover { + background-color: #a8bbbf; + } + + #page-container { + width: 100%; + } + + .body-message img{ + max-height: 12em; + } + + .tooltip:not(.shown) { + display: none; + } + + textarea { + resize: none; + } + + .tooltip { + position: absolute; + z-index: 1000; + } + + #loader { + display: block; + margin-left: auto; + margin-right: auto; + width: 80px; + } + .profile-pic { + height: 2.6em; + width: 2.6em; + border-radius: 0.3em; + margin-top: 0.1em; + float: left; + } + .body-message { + padding-left: 3em; + padding-bottom: 0.5em; + border-bottom: 1px dotted lightgray; + } + .user-time { + margin-bottom: 0.3em; + } + .time { + margin-left: 0.5em; + } + .clear { + clear: both; + } + .content-message { + word-wrap: break-word; + } + .content-message p { + margin: 0; + } + #content { + width: 100%; + } + #content-body { + padding-bottom: 0; + } + #page-container { + min-height: 0; + } + .sidebox h3 { + border-radius: 0; + } + .body-block { + border-radius: 4px; + padding: 0.05em 0.6em; + width: 100%; + } + #search-form { + float: inherit; + } + #search-container { + margin-bottom: 0.4em; + } + #setting { + position: relative; + } + + + @media (min-width: 800px) { + #page-container { + position:fixed; + overflow:hidden; + } + } + @media (max-width: 799px) { + html, body { + max-width: 100%; + overflow-x: hidden; + } + #mobile ul { + width: 100%; + } + .info-pic { + margin-left: 0.5em; + } + .active-span { + display: none; + } + } + +{% endcompress %} \ No newline at end of file diff --git a/templates/chat/chat_js.html b/templates/chat/chat_js.html new file mode 100644 index 0000000..116d949 --- /dev/null +++ b/templates/chat/chat_js.html @@ -0,0 +1,553 @@ + \ No newline at end of file From 9f0213865de5242cfe72d1f2db62e89935229899 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 28 Aug 2023 14:35:44 -0500 Subject: [PATCH 13/42] Move chat template out of compress --- templates/chat/chat.html | 5 +++++ templates/chat/chat_js.html | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/chat/chat.html b/templates/chat/chat.html index 857aaf9..bf65e17 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -42,6 +42,11 @@ return receiver; } + let message_template = ` +{% with message=message_template %} + {% include "chat/message.html" %} +{% endwith %} + `; $(function() { load_dynamic_update({{last_msg}}); }); diff --git a/templates/chat/chat_js.html b/templates/chat/chat_js.html index 116d949..95ca7df 100644 --- a/templates/chat/chat_js.html +++ b/templates/chat/chat_js.html @@ -1,9 +1,4 @@ \ No newline at end of file diff --git a/templates/chat/online_status.html b/templates/chat/online_status.html index edd257b..d82159d 100644 --- a/templates/chat/online_status.html +++ b/templates/chat/online_status.html @@ -27,11 +27,18 @@ fill="{{'green' if user.is_online else 'red'}}"/>
- - {{ user.user.username }} - - - {{user.unread_count if user.unread_count}} +
+ + {{ user.user.username }} + + {% if user.last_msg %} + + {{ user.last_msg }} + + {% endif %} +
+ + {{user.unread_count if user.unread_count}} {% endfor %} From accf5864137aeb18ace364c2b93c5ac8b6aed8eb Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 29 Aug 2023 18:52:24 -0500 Subject: [PATCH 16/42] Add search to internal problem --- judge/views/internal.py | 21 ++++++++++++++------- templates/internal/problem.html | 5 +++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/judge/views/internal.py b/judge/views/internal.py index 4467fc3..eff02a4 100644 --- a/judge/views/internal.py +++ b/judge/views/internal.py @@ -3,7 +3,7 @@ import json from django.views.generic import ListView from django.utils.translation import gettext as _, gettext_lazy -from django.db.models import Count +from django.db.models import Count, Q from django.http import HttpResponseForbidden from django.urls import reverse @@ -38,13 +38,19 @@ class InternalProblem(InternalView, ListView): **kwargs ) + def get_search_query(self): + return self.request.GET.get("q") or self.request.POST.get("q") + def get_queryset(self): - queryset = ( - Problem.objects.annotate(vote_count=Count("volunteer_user_votes")) - .filter(vote_count__gte=1) - .order_by("-vote_count") - ) - return queryset + queryset = Problem.objects.annotate( + vote_count=Count("volunteer_user_votes") + ).filter(vote_count__gte=1) + query = self.get_search_query() + if query: + queryset = queryset.filter( + Q(code__icontains=query) | Q(name__icontains=query) + ) + return queryset.order_by("-vote_count") def get_context_data(self, **kwargs): context = super(InternalProblem, self).get_context_data(**kwargs) @@ -52,6 +58,7 @@ class InternalProblem(InternalView, ListView): context["title"] = self.title context["page_prefix"] = self.request.path + "?page=" context["first_page_href"] = self.request.path + context["query"] = self.get_search_query() return context diff --git a/templates/internal/problem.html b/templates/internal/problem.html index eebb02c..d1147e8 100644 --- a/templates/internal/problem.html +++ b/templates/internal/problem.html @@ -29,6 +29,11 @@ {% endblock %} {% block middle_content %} + + + + +
From 944d3a733ed825a1a843ed44d1708ee9249cb1e5 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 29 Aug 2023 21:50:33 -0500 Subject: [PATCH 17/42] More chat ui --- resources/chatbox.scss | 48 ++++++-------------- resources/darkmode.css | 40 +++++++---------- templates/chat/chat.html | 12 +++-- templates/chat/chat_css.html | 61 +++++++++++++++++++++++++- templates/chat/chat_js.html | 8 +++- templates/chat/user_online_status.html | 2 +- 6 files changed, 107 insertions(+), 64 deletions(-) diff --git a/resources/chatbox.scss b/resources/chatbox.scss index fa37b15..fac7090 100644 --- a/resources/chatbox.scss +++ b/resources/chatbox.scss @@ -11,15 +11,6 @@ float: right; margin-right: 1em; } -#emoji-button { - position: absolute; - right: 1em; - font-size: 2em; - color: lightgray; -} -#emoji-button:hover { - color: gray; -} #chat-log { padding: 0; padding-top: 2em; @@ -58,18 +49,12 @@ overflow-y: scroll; border-bottom-left-radius: 0; border-bottom-right-radius: 0; - height: 75%; + flex-grow: 1; } #chat-input { - width: 100%; - padding: 0.4em 4em 1em 1.2em; - border: 0; color: black; - border-top-left-radius: 0; - border-top-right-radius: 0; - height: 100%; - font-size: 16px; + border: 2px solid #e4a81c; } #chat-online-content { padding: 0; @@ -87,7 +72,7 @@ #chat-container { display: flex; width: 100%; - height: calc(100vh - 3em);; + height: calc(100vh - 3em); border: 1px solid #ccc; /*border-radius: 0 4px 0 0;*/ border-bottom: 0; @@ -99,9 +84,6 @@ #chat-area { flex-grow: 1; } - .chat-left-panel, .chat-right-panel { - display: block !important; - } } #chat-input, #chat-log .content-message { font-family: "Noto Sans", Arial, "Lucida Grande", sans-serif; @@ -109,14 +91,7 @@ .info-pic { height: 100%; } -.info-circle { - position: absolute; - cx: 12%; - cy: 12%; - r: 12%; - stroke: white; - stroke-width: 1; -} + .info-name { margin-left: 10px; font-size: 2em; @@ -173,6 +148,7 @@ display: flex; padding: 15px; gap: 0.5em; + border-radius: 6px; } .status-row:hover { background: lightgray; @@ -191,6 +167,8 @@ border-radius: 15px; max-width: 70%; width: fit-content; + font-size: 1.05rem; + line-height: 1.3; } .message-text-other { background: #eeeeee; @@ -200,7 +178,12 @@ background: rgb(0, 132, 255); color: white; } - +.chat-input-icon { + color: #045343; +} +.chat-input-icon:hover { + background: lightgray; +} .chat { .active-span { color: #636363; @@ -239,9 +222,6 @@ @media (max-width: 799px) { #chat-area { - height: calc(100vh - 50px); - } - #emoji-button { - display: none; + height: calc(100vh - 120px); } } diff --git a/resources/darkmode.css b/resources/darkmode.css index fc2d96d..b0562f9 100644 --- a/resources/darkmode.css +++ b/resources/darkmode.css @@ -3008,19 +3008,13 @@ a.voted { background-image: initial; background-color: rgb(20, 22, 22); } -#emoji-button { - color: rgb(169, 166, 160); -} -#emoji-button:hover { - color: rgb(126, 119, 107); -} #chat-online { border-right-color: rgb(51, 56, 58); border-bottom-color: initial; } #chat-input { - border-color: initial; color: rgb(193, 191, 188); + border-color: rgb(132, 97, 16); } .selected-status-row { background-color: rgb(41, 44, 46); @@ -3036,9 +3030,6 @@ a.voted { border-bottom-color: initial; } } -.info-circle { - stroke: rgb(193, 191, 188); -} #chat-info { box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 3px; } @@ -3068,6 +3059,13 @@ a.voted { background-color: rgb(0, 88, 169); color: rgb(193, 191, 188); } +.chat-input-icon { + color: rgb(170, 166, 161); +} +.chat-input-icon:hover { + background-image: initial; + background-color: rgb(41, 44, 46); +} .chat .active-span { color: rgb(140, 134, 125); } @@ -3728,19 +3726,6 @@ code .il { .sr-only { border-color: initial; } -::-webkit-scrollbar-track { - background-color: transparent; -} -::-webkit-scrollbar-thumb { - background-color: rgb(33, 41, 45); - border-color: transparent; -} -::-webkit-scrollbar-thumb:hover { - background-color: rgb(50, 65, 68); -} -.body-message { - border-bottom-color: rgb(50, 54, 56); -} .CtxtMenu_InfoContent { border-color: initial; background-color: rgb(28, 30, 32); @@ -3838,6 +3823,15 @@ mjx-merror { mjx-assistive-mml { border-color: initial !important; } +mjx-stretchy-v > mjx-ext { + border-color: transparent; +} +.recently-attempted ul { + list-style-image: initial; +} +.organization-row:last-child { + border-bottom-color: initial; +} /* Override Style */ .vimvixen-hint { diff --git a/templates/chat/chat.html b/templates/chat/chat.html index ea1365e..1991402 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -84,18 +84,22 @@
-
+
{% include 'chat/user_online_status.html' %}
-
-
- +
+
+
+
+ +
@@ -319,6 +319,11 @@

+ {% else %} +
+ {{ _('There is no ongoing contest at this time.') }} +
+
{% endif %}

@@ -354,15 +359,17 @@ {% else %} - {{ _('There are no scheduled contests at this time.') }} -
+
+ {{ _('There is no scheduled contest at this time.') }} +
+
{% endif %}
+

+ {{ _('Past Contests') }} +

{% if past_contests %} -

- {{ _('Past Contests') }} -

{% if page_obj and page_obj.num_pages > 1 %}
{% include "list-pages.html" %} @@ -411,6 +418,11 @@ {% include "list-pages.html" %}
{% endif %} + {% else %} +
+ {{ _('There is no past contest.') }} +
+
{% endif %} {% endblock %} From 1749e6480297c9cf183be17c793d67daf69a5434 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 30 Aug 2023 13:07:57 -0500 Subject: [PATCH 19/42] Fix chat css --- judge/views/contests.py | 4 +--- templates/chat/chat_css.html | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/judge/views/contests.py b/judge/views/contests.py index ff4b320..c183435 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -1357,9 +1357,7 @@ class ContestClarificationAjax(ContestMixin, DetailView): raise Http404() polling_time = 1 # minute - last_one_minute = last_five_minutes = timezone.now() - timezone.timedelta( - minutes=polling_time - ) + last_one_minute = timezone.now() - timezone.timedelta(minutes=polling_time) queryset = ContestProblemClarification.objects.filter( problem__in=self.object.contest_problems.all(), date__gte=last_one_minute diff --git a/templates/chat/chat_css.html b/templates/chat/chat_css.html index 5e333bf..ccc2feb 100644 --- a/templates/chat/chat_css.html +++ b/templates/chat/chat_css.html @@ -164,9 +164,10 @@ .info-pic { border-radius: 50%; margin-left: 1em; + width: 3em; } #chat-info { - height: 10%; + height: 3em; } @media (min-width: 800px) { @@ -195,9 +196,6 @@ .active-span { display: none; } - #chat-info { - height: 5%; - } } {% endcompress %} \ No newline at end of file From 1473118c5a316d250087530dc70727f3c719a5e8 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 30 Aug 2023 13:19:51 -0500 Subject: [PATCH 20/42] More css fix for chat --- templates/chat/chat_css.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/chat/chat_css.html b/templates/chat/chat_css.html index ccc2feb..1ae5790 100644 --- a/templates/chat/chat_css.html +++ b/templates/chat/chat_css.html @@ -164,7 +164,6 @@ .info-pic { border-radius: 50%; margin-left: 1em; - width: 3em; } #chat-info { height: 3em; From abbe5f15e1451c3910a2074c6bcdd929f3ce0b6e Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 30 Aug 2023 18:46:47 -0500 Subject: [PATCH 21/42] Add meta address key to setting --- dmoj/settings.py | 3 +++ judge/user_log.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index d03dc27..713e31c 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -487,3 +487,6 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Chat CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U=" + +# Nginx +META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR" diff --git a/judge/user_log.py b/judge/user_log.py index 91a4c67..b8aec43 100644 --- a/judge/user_log.py +++ b/judge/user_log.py @@ -1,4 +1,5 @@ from django.utils.timezone import now +from django.conf import settings from judge.models import Profile @@ -17,8 +18,8 @@ class LogUserAccessMiddleware(object): ): updates = {"last_access": now()} # Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it. - if request.META.get("REMOTE_ADDR"): - updates["ip"] = request.META.get("REMOTE_ADDR") + if request.META.get(settings.META_REMOTE_ADDRESS_KEY): + updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY) Profile.objects.filter(user_id=request.user.pk).update(**updates) return response From b03836715f1ade44393066c0bc61b1c096274923 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 30 Aug 2023 18:48:04 -0500 Subject: [PATCH 22/42] Fix setting --- dmoj/settings.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index 713e31c..d5cfa38 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -477,16 +477,16 @@ ML_OUTPUT_PATH = None # Use subdomain for organizations USE_SUBDOMAIN = False -try: - with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: - exec(f.read(), globals()) -except IOError: - pass - -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - # Chat CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U=" # Nginx META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR" + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +try: + with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: + exec(f.read(), globals()) +except IOError: + pass From 1f03106766d8c281adc906fd399ebbdce4b26f4d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 31 Aug 2023 19:35:56 -0500 Subject: [PATCH 23/42] Add ultimate format --- judge/contest_format/__init__.py | 1 + judge/contest_format/ultimate.py | 55 +++++++++++++++++++ .../0167_ultimate_contest_format.py | 32 +++++++++++ 3 files changed, 88 insertions(+) create mode 100644 judge/contest_format/ultimate.py create mode 100644 judge/migrations/0167_ultimate_contest_format.py diff --git a/judge/contest_format/__init__.py b/judge/contest_format/__init__.py index 8d2ccc9..9f6d628 100644 --- a/judge/contest_format/__init__.py +++ b/judge/contest_format/__init__.py @@ -4,4 +4,5 @@ from judge.contest_format.ecoo import ECOOContestFormat from judge.contest_format.icpc import ICPCContestFormat from judge.contest_format.ioi import IOIContestFormat from judge.contest_format.new_ioi import NewIOIContestFormat +from judge.contest_format.ultimate import UltimateContestFormat from judge.contest_format.registry import choices, formats diff --git a/judge/contest_format/ultimate.py b/judge/contest_format/ultimate.py new file mode 100644 index 0000000..7960d02 --- /dev/null +++ b/judge/contest_format/ultimate.py @@ -0,0 +1,55 @@ +from django.utils.translation import gettext_lazy + +from judge.contest_format.ioi import IOIContestFormat +from judge.contest_format.registry import register_contest_format +from django.db.models import Min, OuterRef, Subquery + +# This contest format only counts last submission for each problem. + + +@register_contest_format("ultimate") +class UltimateContestFormat(IOIContestFormat): + name = gettext_lazy("Ultimate") + + def update_participation(self, participation): + cumtime = 0 + score = 0 + format_data = {} + + queryset = participation.submissions + if self.contest.freeze_after: + queryset = queryset.filter( + submission__date__lt=participation.start + self.contest.freeze_after + ) + + queryset = ( + queryset.values("problem_id") + .filter( + id=Subquery( + queryset.filter(problem_id=OuterRef("problem_id")) + .order_by("-id") + .values("id")[:1] + ) + ) + .values_list("problem_id", "submission__date", "points") + ) + + for problem_id, time, points in queryset: + if self.config["cumtime"]: + dt = (time - participation.start).total_seconds() + if points: + cumtime += dt + else: + dt = 0 + format_data[str(problem_id)] = { + "time": dt, + "points": points, + } + score += points + + self.handle_frozen_state(participation, format_data) + participation.cumtime = max(cumtime, 0) + participation.score = round(score, self.contest.points_precision) + participation.tiebreaker = 0 + participation.format_data = format_data + participation.save() diff --git a/judge/migrations/0167_ultimate_contest_format.py b/judge/migrations/0167_ultimate_contest_format.py new file mode 100644 index 0000000..636e882 --- /dev/null +++ b/judge/migrations/0167_ultimate_contest_format.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.18 on 2023-09-01 00:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0166_display_rank_index"), + ] + + operations = [ + migrations.AlterField( + model_name="contest", + name="format_name", + field=models.CharField( + choices=[ + ("atcoder", "AtCoder"), + ("default", "Default"), + ("ecoo", "ECOO"), + ("icpc", "ICPC"), + ("ioi", "IOI"), + ("ioi16", "New IOI"), + ("ultimate", "Ultimate"), + ], + default="default", + help_text="The contest format module to use.", + max_length=32, + verbose_name="contest format", + ), + ), + ] From 120cc3c06d5b3710a1a7dc1c13aade4e452780fe Mon Sep 17 00:00:00 2001 From: Tran Trong Nghia <80335335+emladevops@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:31:24 +0700 Subject: [PATCH 24/42] Update README.md Profile images nginx guide update. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 416ae9c..a30ddcf 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,14 @@ python3 manage.py runserver 0.0.0.0:8000 1. (WSL) có thể tải ứng dụng Terminal trong Windows Store 2. (WSL) mỗi lần mở ubuntu, các bạn cần chạy lệnh sau để mariadb khởi động: `sudo service mysql restart` (tương tự cho một số service khác như memcached, celery) -3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok. +3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok +4. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code. + +``` + location /profile_images/ { + root /path/to/oj; + } +``` ```jsx . dmojsite/bin/activate From b7c6d45b80ebf7fa0bc29c7a621cd719503ba887 Mon Sep 17 00:00:00 2001 From: Tran Trong Nghia <80335335+emladevops@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:31:59 +0700 Subject: [PATCH 25/42] Update README.md Profile Images configuration --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a30ddcf..e534204 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,9 @@ python3 manage.py runserver 0.0.0.0:8000 4. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code. ``` - location /profile_images/ { - root /path/to/oj; - } +location /profile_images/ { + root /path/to/oj; +} ``` ```jsx From fa21cde2c9fd7bb09a47b4ba6b8594a2b700900c Mon Sep 17 00:00:00 2001 From: Tran Trong Nghia <80335335+emladevops@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:32:41 +0700 Subject: [PATCH 26/42] Update README.md Fix typo --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e534204..ea64e92 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,11 @@ python3 manage.py runserver 0.0.0.0:8000 1. (WSL) có thể tải ứng dụng Terminal trong Windows Store 2. (WSL) mỗi lần mở ubuntu, các bạn cần chạy lệnh sau để mariadb khởi động: `sudo service mysql restart` (tương tự cho một số service khác như memcached, celery) 3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok -4. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code. +```jsx +. dmojsite/bin/activate +python3 manage.py runserver +``` +5. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code. ``` location /profile_images/ { @@ -124,11 +128,6 @@ location /profile_images/ { } ``` -```jsx -. dmojsite/bin/activate -python3 manage.py runserver -``` - 1. Quy trình dev: 1. Sau khi thay đổi code thì django tự build lại, các bạn chỉ cần F5 2. Một số style nằm trong các file .scss. Các bạn cần recompile css thì mới thấy được thay đổi. From 345e9985e3802a89858315a512e988f90bab0d8c Mon Sep 17 00:00:00 2001 From: Tran Trong Nghia <80335335+emladevops@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:34:11 +0700 Subject: [PATCH 27/42] Beautify README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea64e92..d4cdc43 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ location /profile_images/ { } ``` -1. Quy trình dev: +6. Quy trình dev: 1. Sau khi thay đổi code thì django tự build lại, các bạn chỉ cần F5 2. Một số style nằm trong các file .scss. Các bạn cần recompile css thì mới thấy được thay đổi. From 9a89c5a15a83bb9fb9309a535fcff0d4d337d40c Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 1 Sep 2023 17:51:40 -0500 Subject: [PATCH 28/42] Make chat ava square --- templates/chat/chat_css.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/chat/chat_css.html b/templates/chat/chat_css.html index 1ae5790..4909345 100644 --- a/templates/chat/chat_css.html +++ b/templates/chat/chat_css.html @@ -162,7 +162,7 @@ stroke-width: 1; } .info-pic { - border-radius: 50%; + border-radius: 5px; margin-left: 1em; } #chat-info { From 41ba0894acb6dd97427589acbd43910d0555adac Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 1 Sep 2023 18:09:30 -0500 Subject: [PATCH 29/42] Add loading bar --- resources/base.scss | 14 ++++++++++++-- resources/common.js | 7 +++++++ resources/vars.scss | 2 ++ templates/base.html | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/resources/base.scss b/resources/base.scss index 48ac6af..687249f 100644 --- a/resources/base.scss +++ b/resources/base.scss @@ -233,7 +233,7 @@ header { top: 0; left: 0; right: 0; - height: 48px; + height: $navbar_height; } nav { @@ -377,7 +377,7 @@ hr { } #content { - margin: 48px auto 1em auto; + margin: $navbar_height auto 1em auto; padding-top: 1em; // Header @@ -860,6 +860,16 @@ select { margin-bottom: 1em; } +#loading-bar { + position: fixed; + top: 0; + left: 0; + height: 2px; + background-color: #993932; + width: 0; + z-index: 9999; +} + @media (max-width: 799px) { #user-links, .anon { padding-right: 0.5em; diff --git a/resources/common.js b/resources/common.js index 488fb33..380dc63 100644 --- a/resources/common.js +++ b/resources/common.js @@ -385,6 +385,12 @@ function onWindowReady() { showTooltip(e.trigger, fallbackMessage(e.action)); }); }); + $('a').click(function() { + $("#loading-bar").show(); + $("#loading-bar").animate({ width: "100%" }, 1500, function() { + $(this).hide(); + }); + }); } $(function() { @@ -429,4 +435,5 @@ $(function() { $('html').click(function () { $nav_list.hide(); }); + }); \ No newline at end of file diff --git a/resources/vars.scss b/resources/vars.scss index 4bcda31..7556626 100644 --- a/resources/vars.scss +++ b/resources/vars.scss @@ -10,3 +10,5 @@ $widget_border_radius: 4px; $table_header_rounding: 6px; $monospace-fonts: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + +$navbar_height: 48px; diff --git a/templates/base.html b/templates/base.html index 2e5013e..c5be0ff 100644 --- a/templates/base.html +++ b/templates/base.html @@ -316,6 +316,7 @@ +
{% if request.in_contest %}
From 4401fa7376de499141341a053ed9a802badf1819 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 1 Sep 2023 18:20:10 -0500 Subject: [PATCH 30/42] Simplify nav user span --- resources/base.scss | 10 +++++++++- resources/darkmode.css | 18 +++++++++--------- templates/base.html | 4 ++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/resources/base.scss b/resources/base.scss index 687249f..13f9d6d 100644 --- a/resources/base.scss +++ b/resources/base.scss @@ -191,11 +191,19 @@ header { display: block; margin: 0; - & > li > a > span { + & > li > span { height: 36px; padding-top: 8px; display: block; white-space: nowrap; + cursor: pointer; + + &:hover { + border-top: 2px solid #9c3706; + color: black; + background: rgba(255, 255, 255, 0.25); + margin: 0; + } & > img { vertical-align: middle; diff --git a/resources/darkmode.css b/resources/darkmode.css index b0562f9..4dba034 100644 --- a/resources/darkmode.css +++ b/resources/darkmode.css @@ -1541,6 +1541,12 @@ header { #user-links { color: rgb(146, 141, 132); } +#user-links > ul > li > span:hover { + border-top-color: rgb(165, 58, 7); + color: rgb(193, 191, 188); + background-image: initial; + background-color: rgba(20, 22, 22, 0.25); +} #nav-shadow { background-image: linear-gradient(rgb(41, 44, 46), rgba(0, 0, 0, 0)); @@ -1793,6 +1799,9 @@ noscript #noscript { .background-footer { color: rgb(126, 119, 107); } +#loading-bar { + background-color: rgb(101, 38, 33); +} @media (min-width: 800px) { #page-container { background-image: initial; @@ -3823,15 +3832,6 @@ mjx-merror { mjx-assistive-mml { border-color: initial !important; } -mjx-stretchy-v > mjx-ext { - border-color: transparent; -} -.recently-attempted ul { - list-style-image: initial; -} -.organization-row:last-child { - border-bottom-color: initial; -} /* Override Style */ .vimvixen-hint { diff --git a/templates/base.html b/templates/base.html index c5be0ff..70ec640 100644 --- a/templates/base.html +++ b/templates/base.html @@ -261,14 +261,14 @@ {% if request.user.is_authenticated %} -