pull requirement.txt

This commit is contained in:
DELL 2022-10-07 13:25:05 +07:00
commit c44fb6c47a
19 changed files with 479 additions and 263 deletions

View file

@ -891,30 +891,22 @@ urlpatterns = [
"^language/", "^language/",
include( include(
[ [
url("^$", stats.language, name="language_stats"),
url( url(
"^data/all/$", "^$",
stats.language_data, stats.StatLanguage.as_view(),
name="language_stats_data_all", name="language_stats",
),
url(
"^data/ac/$",
stats.ac_language_data,
name="language_stats_data_ac",
),
url(
"^data/status/$",
stats.status_data,
name="stats_data_status",
),
url(
"^data/ac_rate/$",
stats.ac_rate,
name="language_stats_data_ac_rate",
), ),
] ]
), ),
), ),
url(
"^site/",
include(
[
url("^$", stats.StatSite.as_view(), name="site_stats"),
]
),
),
] ]
), ),
), ),

View file

@ -184,10 +184,49 @@ class AddOrganizationForm(ModelForm):
return res return res
class OrganizationContestForm(ModelForm): class AddOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationContestForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
contest = super(AddOrganizationContestForm, self).save(commit=False)
old_save_m2m = self.save_m2m
def save_m2m():
for i, problem in enumerate(self.cleaned_data["problems"]):
contest_problem = ContestProblem(
contest=contest, problem=problem, points=100, order=i + 1
)
contest_problem.save()
contest.contest_problems.add(contest_problem)
old_save_m2m()
self.save_m2m = save_m2m
contest.save()
self.save_m2m()
return contest
class Meta:
model = Contest
fields = (
"key",
"name",
"start_time",
"end_time",
"problems",
)
widgets = {
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
}
class EditOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.org_id = kwargs.pop("org_id", 0) self.org_id = kwargs.pop("org_id", 0)
super(OrganizationContestForm, self).__init__(*args, **kwargs) super(EditOrganizationContestForm, self).__init__(*args, **kwargs)
for field in [ for field in [
"authors", "authors",
"curators", "curators",
@ -203,27 +242,25 @@ class OrganizationContestForm(ModelForm):
class Meta: class Meta:
model = Contest model = Contest
fields = ( fields = (
"is_visible",
"key", "key",
"name", "name",
"start_time",
"end_time",
"format_name",
"authors", "authors",
"curators", "curators",
"testers", "testers",
"is_visible", "time_limit",
"use_clarifications", "use_clarifications",
"hide_problem_tags", "hide_problem_tags",
"scoreboard_visibility", "scoreboard_visibility",
"run_pretests_only", "run_pretests_only",
"points_precision", "points_precision",
"start_time",
"end_time",
"time_limit",
"description", "description",
"og_image", "og_image",
"logo_override_image", "logo_override_image",
"summary", "summary",
"format_name",
"format_config",
"problem_label_script",
"access_code", "access_code",
"private_contestants", "private_contestants",
"view_contest_scoreboard", "view_contest_scoreboard",

View file

@ -5,7 +5,7 @@ from urllib.parse import urlparse
import mistune import mistune
from django.conf import settings from django.conf import settings
from jinja2 import Markup from markupsafe import Markup
from lxml import html from lxml import html
from lxml.etree import ParserError, XMLSyntaxError from lxml.etree import ParserError, XMLSyntaxError

View file

@ -1,7 +1,8 @@
import re import re
from jinja2 import Markup, nodes from jinja2 import nodes
from jinja2.ext import Extension from jinja2.ext import Extension
from markupsafe import Markup
class SpacelessExtension(Extension): class SpacelessExtension(Extension):

View file

@ -152,19 +152,21 @@ class ProblemData(models.Model):
if e.errno != errno.ENOENT: if e.errno != errno.ENOENT:
raise raise
if self.zipfile: if self.zipfile:
self.zipfile.name = _problem_directory_file(new, self.zipfile.name) self.zipfile.name = problem_directory_file_helper(new, self.zipfile.name)
if self.generator: if self.generator:
self.generator.name = _problem_directory_file(new, self.generator.name) self.generator.name = problem_directory_file_helper(
new, self.generator.name
)
if self.custom_checker: if self.custom_checker:
self.custom_checker.name = _problem_directory_file( self.custom_checker.name = problem_directory_file_helper(
new, self.custom_checker.name new, self.custom_checker.name
) )
if self.custom_checker: if self.custom_checker:
self.custom_checker.name = _problem_directory_file( self.custom_checker.name = problem_directory_file_helper(
new, self.custom_checker.name new, self.custom_checker.name
) )
if self.custom_validator: if self.custom_validator:
self.custom_validator.name = _problem_directory_file( self.custom_validator.name = problem_directory_file_helper(
new, self.custom_validator.name new, self.custom_validator.name
) )
self.save() self.save()

View file

@ -47,8 +47,9 @@ from judge.forms import (
AddOrganizationMemberForm, AddOrganizationMemberForm,
OrganizationBlogForm, OrganizationBlogForm,
OrganizationAdminBlogForm, OrganizationAdminBlogForm,
OrganizationContestForm, EditOrganizationContestForm,
ContestProblemFormSet, ContestProblemFormSet,
AddOrganizationContestForm,
) )
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
@ -159,7 +160,9 @@ class OrganizationMixin(OrganizationBase):
class AdminOrganizationMixin(OrganizationMixin): class AdminOrganizationMixin(OrganizationMixin):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
res = super(AdminOrganizationMixin, self).dispatch(request, *args, **kwargs) res = super(AdminOrganizationMixin, self).dispatch(request, *args, **kwargs)
if self.can_edit_organization(self.organization): if not hasattr(self, "organization") or self.can_edit_organization(
self.organization
):
return res return res
return generic_message( return generic_message(
request, request,
@ -172,7 +175,7 @@ class AdminOrganizationMixin(OrganizationMixin):
class MemberOrganizationMixin(OrganizationMixin): class MemberOrganizationMixin(OrganizationMixin):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
res = super(MemberOrganizationMixin, self).dispatch(request, *args, **kwargs) res = super(MemberOrganizationMixin, self).dispatch(request, *args, **kwargs)
if self.can_access(self.organization): if not hasattr(self, "organization") or self.can_access(self.organization):
return res return res
return generic_message( return generic_message(
request, request,
@ -389,18 +392,12 @@ class OrganizationContestMixin(
OrganizationHomeViewContext, OrganizationHomeViewContext,
): ):
model = Contest model = Contest
form_class = OrganizationContestForm
def is_contest_editable(self, request, contest): def is_contest_editable(self, request, contest):
return request.profile in contest.authors.all() or self.can_edit_organization( return request.profile in contest.authors.all() or self.can_edit_organization(
self.organization self.organization
) )
def get_form_kwargs(self):
kwargs = super(OrganizationContestMixin, self).get_form_kwargs()
kwargs["org_id"] = self.organization.id
return kwargs
class OrganizationContests( class OrganizationContests(
OrganizationContestMixin, MemberOrganizationMixin, ContestList OrganizationContestMixin, MemberOrganizationMixin, ContestList
@ -862,17 +859,26 @@ class AddOrganizationContest(
AdminOrganizationMixin, OrganizationContestMixin, CreateView AdminOrganizationMixin, OrganizationContestMixin, CreateView
): ):
template_name = "organization/contest/add.html" template_name = "organization/contest/add.html"
form_class = AddOrganizationContestForm
def get_title(self): def get_title(self):
return _("Add contest") return _("Add contest")
def get_form_kwargs(self):
kwargs = super(AddOrganizationContest, self).get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_("Added from site")) revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user) revisions.set_user(self.request.user)
res = super(AddOrganizationContest, self).form_valid(form) res = super(AddOrganizationContest, self).form_valid(form)
self.object.organizations.add(self.organization) self.object.organizations.add(self.organization)
self.object.is_organization_private = True self.object.is_organization_private = True
self.object.authors.add(self.request.profile)
self.object.save() self.object.save()
return res return res
@ -886,7 +892,8 @@ class AddOrganizationContest(
class EditOrganizationContest( class EditOrganizationContest(
OrganizationContestMixin, MemberOrganizationMixin, UpdateView OrganizationContestMixin, MemberOrganizationMixin, UpdateView
): ):
template_name = "organization/contest/add.html" template_name = "organization/contest/edit.html"
form_class = EditOrganizationContestForm
def setup_contest(self, request, *args, **kwargs): def setup_contest(self, request, *args, **kwargs):
contest_key = kwargs.get("contest", None) contest_key = kwargs.get("contest", None)
@ -903,6 +910,11 @@ class EditOrganizationContest(
status=400, status=400,
) )
def get_form_kwargs(self):
kwargs = super(EditOrganizationContest, self).get_form_kwargs()
kwargs["org_id"] = self.organization.id
return kwargs
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
res = self.setup_contest(request, *args, **kwargs) res = self.setup_contest(request, *args, **kwargs)
if res: if res:

View file

@ -845,8 +845,10 @@ class ProblemFeed(ProblemList):
if self.feed_type == "new": if self.feed_type == "new":
return queryset.order_by("-date") return queryset.order_by("-date")
elif user and self.feed_type == "volunteer": elif user and self.feed_type == "volunteer":
voted_problems = user.volunteer_problem_votes.values_list( voted_problems = (
"problem", flat=True user.volunteer_problem_votes.values_list("problem", flat=True)
if not bool(self.search_query)
else []
) )
if self.show_solved_only: if self.show_solved_only:
queryset = queryset.filter( queryset = queryset.filter(

View file

@ -1,5 +1,7 @@
from itertools import chain, repeat from itertools import chain, repeat
from operator import itemgetter from operator import itemgetter
import json
import pandas as pd
from django.conf import settings from django.conf import settings
from django.db.models import Case, Count, FloatField, IntegerField, Value, When from django.db.models import Case, Count, FloatField, IntegerField, Value, When
@ -7,6 +9,10 @@ from django.db.models.expressions import CombinedExpression
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.http import Http404
from django.views.generic import TemplateView
from django.utils.safestring import mark_safe
from django.db import connection
from judge.models import Language, Submission from judge.models import Language, Submission
from judge.utils.stats import ( from judge.utils.stats import (
@ -17,28 +23,34 @@ from judge.utils.stats import (
) )
ac_count = Count( class StatViewBase(TemplateView):
Case(When(submission__result="AC", then=Value(1)), output_field=IntegerField()) def get(self, request, *args, **kwargs):
) if not request.user.is_superuser:
raise Http404
return super().get(request, *args, **kwargs)
def repeat_chain(iterable): class StatLanguage(StatViewBase):
return chain.from_iterable(repeat(iterable)) template_name = "stats/language.html"
ac_count = Count(
Case(When(submission__result="AC", then=Value(1)), output_field=IntegerField())
def language_data(
request, language_count=Language.objects.annotate(count=Count("submission"))
):
languages = (
language_count.filter(count__gt=0)
.values("key", "name", "count")
.order_by("-count")
) )
num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD)
other_count = sum(map(itemgetter("count"), languages[num_languages:]))
return JsonResponse( def repeat_chain(iterable):
{ return chain.from_iterable(repeat(iterable))
def language_data(
self, language_count=Language.objects.annotate(count=Count("submission"))
):
languages = (
language_count.filter(count__gt=0)
.values("key", "name", "count")
.order_by("-count")
)
num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD)
other_count = sum(map(itemgetter("count"), languages[num_languages:]))
return {
"labels": list(map(itemgetter("name"), languages[:num_languages])) "labels": list(map(itemgetter("name"), languages[:num_languages]))
+ ["Other"], + ["Other"],
"datasets": [ "datasets": [
@ -50,53 +62,139 @@ def language_data(
+ [other_count], + [other_count],
}, },
], ],
}, }
safe=False,
)
def ac_language_data(self):
return self.language_data(Language.objects.annotate(count=self.ac_count))
def ac_language_data(request): def status_data(self, statuses=None):
return language_data(request, Language.objects.annotate(count=ac_count)) if not statuses:
statuses = (
Submission.objects.values("result")
.annotate(count=Count("result"))
.values("result", "count")
.order_by("-count")
)
data = []
for status in statuses:
res = status["result"]
if not res:
continue
count = status["count"]
data.append((str(Submission.USER_DISPLAY_CODES[res]), count))
return get_pie_chart(data)
def status_data(request, statuses=None): def ac_rate(self):
if not statuses: rate = CombinedExpression(
statuses = ( self.ac_count / Count("submission"),
Submission.objects.values("result") "*",
.annotate(count=Count("result")) Value(100.0),
.values("result", "count") output_field=FloatField(),
.order_by("-count")
) )
data = [] data = (
for status in statuses: Language.objects.annotate(total=Count("submission"), ac_rate=rate)
res = status["result"] .filter(total__gt=0)
if not res: .order_by("total")
continue .values_list("name", "ac_rate")
count = status["count"] )
data.append((str(Submission.USER_DISPLAY_CODES[res]), count)) return get_bar_chart(list(data))
return JsonResponse(get_pie_chart(data), safe=False) def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Language statistics")
context["tab"] = "language"
context["data_all"] = mark_safe(json.dumps(self.language_data()))
context["lang_ac"] = mark_safe(json.dumps(self.ac_language_data()))
context["status_counts"] = mark_safe(json.dumps(self.status_data()))
context["ac_rate"] = mark_safe(json.dumps(self.ac_rate()))
return context
def ac_rate(request): def group_data_by_period(dates, data, period="1W"):
rate = CombinedExpression( df = pd.DataFrame()
ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField() df["dates"] = pd.to_datetime(dates)
) df["data"] = data
data = ( df = df.groupby([pd.Grouper(key="dates", freq=period)])["data"].mean().reset_index()
Language.objects.annotate(total=Count("submission"), ac_rate=rate) return list(df["dates"]), list(df["data"])
.filter(total__gt=0)
.order_by("total")
.values_list("name", "ac_rate")
)
return JsonResponse(get_bar_chart(list(data)))
def language(request): class StatSite(StatViewBase):
return render( template_name = "stats/site.html"
request,
"stats/language.html", def create_dataset(self, query, label):
{ labels = []
"title": _("Language statistics"), data = []
"tab": "language", with connection.cursor() as cursor:
}, cursor.execute(query)
) for row in cursor.fetchall():
data.append(row[0])
labels.append(row[1])
labels, data = group_data_by_period(labels, data, self.period)
res = {
"labels": labels,
"datasets": [
{
"label": label,
"data": data,
"borderColor": "rgb(75, 192, 192)",
"pointRadius": 1,
"borderWidth": 2,
}
],
}
return mark_safe(json.dumps(res, default=str))
def get_submissions(self):
query = """
SELECT COUNT(*), CAST(date AS Date) as day from judge_submission GROUP BY day;
"""
return self.create_dataset(query, _("Submissions"))
def get_comments(self):
query = """
SELECT COUNT(*), CAST(time AS Date) as day from judge_comment GROUP BY day;
"""
return self.create_dataset(query, _("Comments"))
def get_new_users(self):
query = """
SELECT COUNT(*), CAST(date_joined AS Date) as day from auth_user GROUP BY day;
"""
return self.create_dataset(query, _("New users"))
def get_chat_messages(self):
query = """
SELECT COUNT(*), CAST(time AS Date) as day from chat_box_message GROUP BY day;
"""
return self.create_dataset(query, _("Chat messages"))
def get_contests(self):
query = """
SELECT COUNT(*), CAST(start_time AS Date) as day from judge_contest GROUP BY day;
"""
return self.create_dataset(query, _("Contests"))
def get_groups(self):
query = """
SELECT COUNT(*), CAST(creation_date AS Date) as day from judge_organization GROUP BY day;
"""
return self.create_dataset(query, _("Groups"))
def get(self, request, *args, **kwargs):
self.period = request.GET.get("period", "1W")
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["tab"] = "site"
context["title"] = _("Site statistics")
context["submissions"] = self.get_submissions()
context["comments"] = self.get_comments()
context["new_users"] = self.get_new_users()
context["chat_messages"] = self.get_chat_messages()
context["contests"] = self.get_contests()
context["groups"] = self.get_groups()
return context

View file

@ -1,86 +1,41 @@
amqp==5.1.1 Django>=2.2,<3
ansi2html @ git+https://github.com/DMOJ/ansi2html.git@1791b4a468942eba19cb29242d9075f5e4284450 django_compressor
async-timeout==4.0.2 django-mptt
billiard==3.6.4.0 django-pagedown
celery==5.2.7 django-registration-redux
certifi==2022.6.15.1 django-reversion
cffi==1.15.1 django-reversion-compare
chardet==5.0.0 django-social-share
charset-normalizer==2.1.1 django-sortedm2m @ git+https://github.com/DMOJ/django-sortedm2m.git
click==8.1.3 django-impersonate
click-didyoumean==0.3.0 dmoj-wpadmin @ git+https://github.com/LQDJudge/dmoj-wpadmin.git
click-plugins==1.1.1 lxml
click-repl==0.2.0 Pygments
cryptography==38.0.1 mistune<2
defusedxml==0.7.1 social-auth-app-django
Deprecated==1.2.13 pytz
diff-match-patch==20200713 django-statici18n
Django==2.2.28 pika
django-appconf==1.0.5 ua-parser
django-compressor==4.1 pyyaml
django-fernet-fields==0.6 jinja2
django-impersonate==1.8.2 django_jinja
django-jinja==2.10.2 llist
django-js-asset==2.0.0 requests
django-mptt==0.13.4 django-fernet-fields
django-newsletter==0.9.1 pyotp
django-pagedown==2.2.1 qrcode[pil]
django-registration-redux==2.11 jsonfield
django-reversion==5.0.2 pymoss
django-reversion-compare==0.15.0 packaging
django-social-share==2.3.0 celery
django-sortedm2m==2.0.0 ansi2html @ git+https://github.com/DMOJ/ansi2html.git
django-statici18n==2.3.1 sqlparse
dmoj-wpadmin==1.10.2 django-newsletter
Flask==2.2.2 netaddr
idna==3.3 redis
itsdangerous==2.1.2 lupa
Jinja2==3.1.2 websocket-client
jsonfield==3.1.0 python-memcached
kombu==5.2.4 numpy
ldif3==3.1.1 pandas
llist==0.7.1
lupa==1.13
lxml==4.9.1
MarkupSafe==2.1.1
mistune==0.8.4
mysqlclient==2.1.1
netaddr==0.8.0
numpy==1.23.3
oauthlib==3.2.1
packaging==21.3
pika==1.3.0
Pillow==9.2.0
prompt-toolkit==3.0.31
pycparser==2.21
Pygments==2.13.0
PyJWT==2.4.0
pymoss==0.2
pyotp==2.7.0
pyparsing==3.0.9
python-card-me==0.9.3
python-dateutil==2.8.2
python-memcached==1.59
python3-openid==3.2.0
pytz==2022.2.1
PyYAML==6.0
qrcode==7.3.1
rcssmin==1.1.0
redis==4.3.4
requests==2.28.1
requests-oauthlib==1.3.1
rjsmin==1.2.0
six==1.16.0
social-auth-app-django==5.0.0
social-auth-core==4.3.0
sorl-thumbnail==12.9.0
sqlparse==0.4.2
surlex==0.2.0
ua-parser==0.16.1
unicodecsv==0.14.1
urllib3==1.26.12
vine==5.0.0
wcwidth==0.2.5
websocket-client==1.4.1
Werkzeug==2.2.2
wrapt==1.14.1

View file

@ -5,6 +5,7 @@
<a target="_blank" href="">LQDOJ (Le Quy Don Online Judge)</a> là một trang web chấm bài tự động được phát triển dựa trên nền tảng mã nguồn mở <a target="_blank" href="https://dmoj.ca/">DMOJ</a>. Được xây dựng với mục đích ban đầu là tạo ra một môi trường học tập cho học sinh khối chuyên Tin <a target="_blank" href="http://thpt-lequydon-danang.edu.vn/">trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng)</a>, hiện nay LQDOJ đã cho phép đăng ký tự do để trở thành một sân chơi rộng mở cho toàn bộ cộng đồng học sinh yêu Tin học. Trang web cung cấp lượng bài luyện tập đồ sộ từ các kỳ thi HSG Quốc Gia, ACM ICPC, Olympic Duyên Hải Bắc Bộ, etc. cho đến các contest định kỳ để xếp loại khả năng (rating) giúp các bạn có thêm động lực cạnh tranh và khí thế phấn đấu rèn luyện nâng cao trình độ lập trình. Các bạn có thể tham khảo mã nguồn của trang web tại <a target="_blank" href="https://github.com/LQDJudge/online-judge">Github repo chính thức</a>. Mọi ý kiến đóng góp và thắc mắc xin gửi về: <a target="_blank" href="">LQDOJ (Le Quy Don Online Judge)</a> là một trang web chấm bài tự động được phát triển dựa trên nền tảng mã nguồn mở <a target="_blank" href="https://dmoj.ca/">DMOJ</a>. Được xây dựng với mục đích ban đầu là tạo ra một môi trường học tập cho học sinh khối chuyên Tin <a target="_blank" href="http://thpt-lequydon-danang.edu.vn/">trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng)</a>, hiện nay LQDOJ đã cho phép đăng ký tự do để trở thành một sân chơi rộng mở cho toàn bộ cộng đồng học sinh yêu Tin học. Trang web cung cấp lượng bài luyện tập đồ sộ từ các kỳ thi HSG Quốc Gia, ACM ICPC, Olympic Duyên Hải Bắc Bộ, etc. cho đến các contest định kỳ để xếp loại khả năng (rating) giúp các bạn có thêm động lực cạnh tranh và khí thế phấn đấu rèn luyện nâng cao trình độ lập trình. Các bạn có thể tham khảo mã nguồn của trang web tại <a target="_blank" href="https://github.com/LQDJudge/online-judge">Github repo chính thức</a>. Mọi ý kiến đóng góp và thắc mắc xin gửi về:
<ul> <ul>
<li>Thầy <a target="_blank" href="https://www.facebook.com/dovannho">Đỗ Văn Nhỏ</a> (handle: <span class="rating rate-none user"><a target="_blank" href="../user/Small">Small</a></span>), giáo viên Tin học trường THPT chuyên Lê Quý Đôn, email: <a href="mailto:admin@lqdoj.edu.vn">admin@lqdoj.edu.vn</a></li> <li>Thầy <a target="_blank" href="https://www.facebook.com/dovannho">Đỗ Văn Nhỏ</a> (handle: <span class="rating rate-none user"><a target="_blank" href="../user/Small">Small</a></span>), giáo viên Tin học trường THPT chuyên Lê Quý Đôn, email: <a href="mailto:admin@lqdoj.edu.vn">admin@lqdoj.edu.vn</a></li>
<li><a target="_blank" href="https://www.facebook.com/floweronstone">Nguyễn Đức Thuận</a> (handle: <span class="rating rate-master user"><a target="_blank" href="../user/Flower_On_Stone">Flower_On_Stone</a></span>), email: <a href="mailto:thuanbn03@gmail.com">thuanbn03@gmail.com</a></li>
<li><a target="_blank" href="https://www.facebook.com/profile.php?id=100011662657075">Lê Phước Định</a> (handle: <span class="rating rate-grandmaster user"><a target="_blank" href="../user/cuom1999">cuom1999</a></span>), email: <a href="mailto:dinh@lqdoj.edu.vn">dinh@lqdoj.edu.vn</a></li> <li><a target="_blank" href="https://www.facebook.com/profile.php?id=100011662657075">Lê Phước Định</a> (handle: <span class="rating rate-grandmaster user"><a target="_blank" href="../user/cuom1999">cuom1999</a></span>), email: <a href="mailto:dinh@lqdoj.edu.vn">dinh@lqdoj.edu.vn</a></li>
<li><a target="_blank" href="https://www.facebook.com/doannguyenthanhluong">Đoàn Nguyễn Thành Lương</a> (handle: <span class="rating rate-master user"><a target="_blank" href="../user/CaiWinDao">CaiWinDao</a></span>), email: <a href="mailto:luong@lqdoj.edu.vn">luong@lqdoj.edu.vn</a></li> <li><a target="_blank" href="https://www.facebook.com/doannguyenthanhluong">Đoàn Nguyễn Thành Lương</a> (handle: <span class="rating rate-master user"><a target="_blank" href="../user/CaiWinDao">CaiWinDao</a></span>), email: <a href="mailto:luong@lqdoj.edu.vn">luong@lqdoj.edu.vn</a></li>
</ul> </ul>

View file

@ -282,6 +282,7 @@
{% endif %} {% endif %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<li><a href="{{ url('internal_problem') }}">{{ _('Internal') }}</a></li> <li><a href="{{ url('internal_problem') }}">{{ _('Internal') }}</a></li>
<li><a href="{{ url('site_stats') }}">{{ _('Stats') }}</a></li>
{% endif %} {% endif %}
<li><a href="{{ url('user_edit_profile') }}">{{ _('Edit profile') }}</a></li> <li><a href="{{ url('user_edit_profile') }}">{{ _('Edit profile') }}</a></li>
{% if request.user.is_impersonate %} {% if request.user.is_impersonate %}

View file

@ -6,28 +6,6 @@
{% block three_col_media %} {% block three_col_media %}
{{ form.media.css }} {{ form.media.css }}
<style>
#org-field-wrapper-scoreboard_visibility,
#org-field-wrapper-points_precision,
#org-field-wrapper-start_time,
#org-field-wrapper-end_time,
#org-field-wrapper-time_limit,
#org-field-wrapper-format_name {
display: inline-flex;
}
.problems-problem {
width: 40%;
}
input[type=number] {
width: 5em;
}
.middle-content {
z-index: 1;
}
#three-col-container {
overflow: auto;
}
</style>
{% endblock %} {% endblock %}
{% block middle_content %} {% block middle_content %}
@ -55,36 +33,6 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if problems_form %}
<hr><br>
{{ problems_form.management_form }}
<i>{{_('If you run out of rows, click Save')}}</i>
<table class="table">
<thead>
<tr>
{% for field in problems_form[0] %}
{% if not field.is_hidden %}
<th>
{{field.label}}
</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
{% for form in problems_form %}
<tr>
{% for field in form %}
<td class="problems-{{field.name}}" title="
{{ field.help_text|safe if field.help_text }}"
style="{{ 'display:none' if field.is_hidden }}"
>{{field}}<div style="color:red">{{field.errors}}</div></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<button type="submit">{{ _('Save') }}</button> <button type="submit">{{ _('Save') }}</button>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,88 @@
{% extends "organization/home-base.html" %}
{% block three_col_js %}
{{ form.media.js }}
{% endblock %}
{% block three_col_media %}
{{ form.media.css }}
<style>
#org-field-wrapper-scoreboard_visibility,
#org-field-wrapper-points_precision,
#org-field-wrapper-start_time,
#org-field-wrapper-end_time,
#org-field-wrapper-time_limit,
#org-field-wrapper-format_name {
display: inline-flex;
}
.problems-problem {
width: 40%;
}
input[type=number] {
width: 5em;
}
.middle-content {
z-index: 1;
}
#three-col-container {
overflow: auto;
}
</style>
{% endblock %}
{% block middle_content %}
<form action="" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-dismissable">
<a href="#" class="close">x</a>
{{ form.non_field_errors() }}
{{ form.errors }}
</div>
{% endif %}
{% for field in form %}
{% if not field.is_hidden %}
<div style="margin-bottom: 1em;">
{{ field.errors }}
<label for="{{field.id_for_label }}"><b>{{ field.label }}{% if field.field.required %}<span style="color:red"> * </span>{% endif %}:</b> </label>
<div class="org-field-wrapper" id="org-field-wrapper-{{field.html_name }}">
{{ field }}
</div>
{% if field.help_text %}
<i style="display: block">{{ field.help_text|safe }}</i>
{% endif %}
</div>
{% endif %}
{% endfor %}
<hr><br>
{{ problems_form.management_form }}
<i>{{_('If you run out of rows, click Save')}}</i>
<table class="table">
<thead>
<tr>
{% for field in problems_form[0] %}
{% if not field.is_hidden %}
<th>
{{field.label}}
</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
{% for form in problems_form %}
<tr>
{% for field in form %}
<td class="problems-{{field.name}}" title="
{{ field.help_text|safe if field.help_text }}"
style="{{ 'display:none' if field.is_hidden }}"
>{{field}}<div style="color:red">{{field.errors}}</div></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit">{{ _('Save') }}</button>
</form>
{% endblock %}

View file

@ -6,13 +6,10 @@
{% endcompress %} {% endcompress %}
{% endblock %} {% endblock %}
{% block content_title %}{{ _('Statistics') }}{% endblock %} {% block content_title %}{% endblock %}
{% block title_ruler %}{% endblock %}
{% block body %} {% block body %}
<div class="tabs"> {% include "stats/tab.html" %}
<li{% if tab == 'language' %} class="active"{% endif %}>
<a href="{{ url('language_stats') }}">{{ _('Language') }}</a>
</li>
</div>
{% block chart_body %}{% endblock %} {% block chart_body %}{% endblock %}
{% endblock %} {% endblock %}

View file

@ -36,20 +36,10 @@
Chart.defaults.global.tooltipFontFamily = Chart.defaults.global.tooltipFontFamily =
Chart.defaults.global.tooltipTitleFontFamily = Chart.defaults.global.tooltipTitleFontFamily =
$('body').css('font-family'); $('body').css('font-family');
draw_pie_chart({{ data_all }}, $('#lang-all'));
function pie_chart(url, $chart) { draw_pie_chart({{ lang_ac }}, $('#lang-ac'));
$.getJSON(url, function (data) { draw_pie_chart({{ status_counts }}, $('#status-counts'));
draw_pie_chart(data, $chart); draw_bar_chart({{ ac_rate }}, $('#ac-rate'));
});
}
pie_chart('{{ url('language_stats_data_all') }}', $('#lang-all'));
pie_chart('{{ url('language_stats_data_ac') }}', $('#lang-ac'));
pie_chart('{{ url('stats_data_status') }}', $('#status-counts'));
$.getJSON('{{ url('language_stats_data_ac_rate') }}', function (data) {
draw_bar_chart(data, $('#ac-rate'));
});
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -87,4 +87,24 @@
}); });
return chart; return chart;
} }
function draw_timeline(data, $chart) {
var ctx = $chart.find('canvas')[0].getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
data: data,
options: {
maintainAspectRatio: false,
legend: {
display: false,
},
scales: {
xAxes: [{
type: 'time',
}]
},
},
});
return chart;
}
</script> </script>

65
templates/stats/site.html Normal file
View file

@ -0,0 +1,65 @@
{% extends "stats/base.html" %}
{% block media %}
<style>
.graph {
padding-bottom: 3em;
}
</style>
{% endblock %}
{% block chart_body %}
<div class="graph">
<div id="submissions" class="chart">
<canvas width="90%" height="300"></canvas>
</div>
<center><h3>{{ _('Submissions') }}</h3></center>
</div>
<div class="graph">
<div id="contests" class="chart">
<canvas width="90%" height="300"></canvas>
</div>
<center><h3>{{ _('Contests') }}</h3></center>
</div>
<div class="graph">
<div id="new_users" class="chart">
<canvas width="90%" height="300"></canvas>
</div>
<center><h3>{{ _('New users') }}</h3></center>
</div>
<div class="graph">
<div id="groups" class="chart">
<canvas width="90%" height="300"></canvas>
</div>
<center><h3>{{ _('Groups') }}</h3></center>
</div>
<div class="graph">
<div id="comments" class="chart">
<canvas width="90%" height="300"></canvas>
</div>
<center><h3>{{ _('Comments') }}</h3></center>
</div>
<div class="graph">
<div id="chat_messages" class="chart">
<canvas width="90%" height="300"></canvas>
</div>
<center><h3>{{ _('Chat messages') }}</h3></center>
</div>
{% endblock %}
{% block bodyend %}
<script type="text/javascript">
$(function () {
Chart.defaults.global.scaleFontFamily =
Chart.defaults.global.tooltipFontFamily =
Chart.defaults.global.tooltipTitleFontFamily =
$('body').css('font-family');
draw_timeline({{submissions}}, $('#submissions'));
draw_timeline({{new_users}}, $('#new_users'));
draw_timeline({{comments}}, $('#comments'));
draw_timeline({{chat_messages}}, $('#chat_messages'));
draw_timeline({{contests}}, $('#contests'));
draw_timeline({{groups}}, $('#groups'));
});
</script>
{% endblock %}

6
templates/stats/tab.html Normal file
View file

@ -0,0 +1,6 @@
{% extends "tabs-base.html" %}
{% block tabs %}
{{ make_tab('language', 'fa-list', url('language_stats'), _('Language')) }}
{{ make_tab('site', 'fa-list', url('site_stats'), _('Site')) }}
{% endblock %}

View file

@ -1,4 +1,5 @@
<input <input
autocomplete="off"
type="{{ widget.type }}" type="{{ widget.type }}"
name="{{ widget.name }}" name="{{ widget.name }}"
{% if widget.value != None %} {% if widget.value != None %}