From 9e336a7bc95e29864efeae4665178ac201242974 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 16 Sep 2022 00:07:27 -0500 Subject: [PATCH 1/9] Change add contest form --- judge/forms.py | 55 ++++++++++++--- judge/views/organization.py | 28 +++++--- templates/organization/contest/add.html | 52 -------------- templates/organization/contest/edit.html | 88 ++++++++++++++++++++++++ templates/widgets/datetimepicker.html | 1 + 5 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 templates/organization/contest/edit.html diff --git a/judge/forms.py b/judge/forms.py index 94516c5..f155d49 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -184,10 +184,49 @@ class AddOrganizationForm(ModelForm): 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): self.org_id = kwargs.pop("org_id", 0) - super(OrganizationContestForm, self).__init__(*args, **kwargs) + super(EditOrganizationContestForm, self).__init__(*args, **kwargs) for field in [ "authors", "curators", @@ -203,27 +242,25 @@ class OrganizationContestForm(ModelForm): class Meta: model = Contest fields = ( + "is_visible", "key", "name", + "start_time", + "end_time", + "format_name", "authors", "curators", "testers", - "is_visible", + "time_limit", "use_clarifications", "hide_problem_tags", "scoreboard_visibility", "run_pretests_only", "points_precision", - "start_time", - "end_time", - "time_limit", "description", "og_image", "logo_override_image", "summary", - "format_name", - "format_config", - "problem_label_script", "access_code", "private_contestants", "view_contest_scoreboard", diff --git a/judge/views/organization.py b/judge/views/organization.py index a531d74..c251bc5 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -44,8 +44,9 @@ from judge.forms import ( AddOrganizationMemberForm, OrganizationBlogForm, OrganizationAdminBlogForm, - OrganizationContestForm, + EditOrganizationContestForm, ContestProblemFormSet, + AddOrganizationContestForm, ) from judge.models import ( BlogPost, @@ -126,7 +127,6 @@ class OrganizationMixin(OrganizationBase): context["logo_override_image"] = self.organization.logo_override_image if "organizations" in context: context.pop("organizations") - print(context) return context def dispatch(self, request, *args, **kwargs): @@ -387,18 +387,12 @@ class OrganizationContestMixin( OrganizationHomeViewContext, ): model = Contest - form_class = OrganizationContestForm def is_contest_editable(self, request, contest): return request.profile in contest.authors.all() or self.can_edit_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( OrganizationContestMixin, MemberOrganizationMixin, ContestList @@ -432,7 +426,6 @@ class OrganizationContests( self.set_editable_contest(contest) for contest in context["future_contests"]: self.set_editable_contest(contest) - print(context) return context @@ -861,17 +854,26 @@ class AddOrganizationContest( AdminOrganizationMixin, OrganizationContestMixin, CreateView ): template_name = "organization/contest/add.html" + form_class = AddOrganizationContestForm def get_title(self): 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): with transaction.atomic(), revisions.create_revision(): revisions.set_comment(_("Added from site")) revisions.set_user(self.request.user) + res = super(AddOrganizationContest, self).form_valid(form) + self.object.organizations.add(self.organization) self.object.is_organization_private = True + self.object.authors.add(self.request.profile) self.object.save() return res @@ -885,7 +887,8 @@ class AddOrganizationContest( class EditOrganizationContest( 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): contest_key = kwargs.get("contest", None) @@ -902,6 +905,11 @@ class EditOrganizationContest( 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): res = self.setup_contest(request, *args, **kwargs) if res: diff --git a/templates/organization/contest/add.html b/templates/organization/contest/add.html index ff24057..516d77b 100644 --- a/templates/organization/contest/add.html +++ b/templates/organization/contest/add.html @@ -6,28 +6,6 @@ {% block three_col_media %} {{ form.media.css }} - {% endblock %} {% block middle_content %} @@ -55,36 +33,6 @@ {% endif %} {% endfor %} - {% if problems_form %} -

- {{ problems_form.management_form }} - {{_('If you run out of rows, click Save')}} - - - - {% for field in problems_form[0] %} - {% if not field.is_hidden %} - - {% endif %} - {% endfor %} - - - - {% for form in problems_form %} - - {% for field in form %} - - {% endfor %} - - {% endfor %} - -
- {{field.label}} -
{{field}}
{{field.errors}}
- {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/organization/contest/edit.html b/templates/organization/contest/edit.html new file mode 100644 index 0000000..232b0c0 --- /dev/null +++ b/templates/organization/contest/edit.html @@ -0,0 +1,88 @@ +{% extends "organization/home-base.html" %} + +{% block three_col_js %} + {{ form.media.js }} +{% endblock %} + +{% block three_col_media %} + {{ form.media.css }} + +{% endblock %} + +{% block middle_content %} +
+ {% csrf_token %} + {% if form.errors %} +
+ x + {{ form.non_field_errors() }} + {{ form.errors }} +
+ {% endif %} + {% for field in form %} + {% if not field.is_hidden %} +
+ {{ field.errors }} + +
+ {{ field }} +
+ {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+ {% endif %} + {% endfor %} + +

+ {{ problems_form.management_form }} + {{_('If you run out of rows, click Save')}} + + + + {% for field in problems_form[0] %} + {% if not field.is_hidden %} + + {% endif %} + {% endfor %} + + + + {% for form in problems_form %} + + {% for field in form %} + + {% endfor %} + + {% endfor %} + +
+ {{field.label}} +
{{field}}
{{field.errors}}
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/widgets/datetimepicker.html b/templates/widgets/datetimepicker.html index 9363d07..e09b77a 100644 --- a/templates/widgets/datetimepicker.html +++ b/templates/widgets/datetimepicker.html @@ -1,4 +1,5 @@ Date: Fri, 16 Sep 2022 18:02:53 -0500 Subject: [PATCH 2/9] Import Markup from markupsafe --- judge/jinja2/markdown/__init__.py | 2 +- judge/jinja2/spaceless.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/judge/jinja2/markdown/__init__.py b/judge/jinja2/markdown/__init__.py index 3387ddf..e22ae61 100644 --- a/judge/jinja2/markdown/__init__.py +++ b/judge/jinja2/markdown/__init__.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse import mistune from django.conf import settings -from jinja2 import Markup +from markupsafe import Markup from lxml import html from lxml.etree import ParserError, XMLSyntaxError diff --git a/judge/jinja2/spaceless.py b/judge/jinja2/spaceless.py index 0644e89..01eb5d8 100644 --- a/judge/jinja2/spaceless.py +++ b/judge/jinja2/spaceless.py @@ -1,7 +1,8 @@ import re -from jinja2 import Markup, nodes +from jinja2 import nodes from jinja2.ext import Extension +from markupsafe import Markup class SpacelessExtension(Extension): From 8459d3c6e6ee4e089616c546e1dabb7051ab1416 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 21 Sep 2022 22:21:05 -0500 Subject: [PATCH 3/9] Fix problem code bug --- judge/models/problem_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py index d1b9204..1469c3f 100644 --- a/judge/models/problem_data.py +++ b/judge/models/problem_data.py @@ -152,19 +152,19 @@ class ProblemData(models.Model): if e.errno != errno.ENOENT: raise 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: - 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: - self.custom_checker.name = _problem_directory_file( + self.custom_checker.name = problem_directory_file_helper( new, self.custom_checker.name ) if self.custom_checker: - self.custom_checker.name = _problem_directory_file( + self.custom_checker.name = problem_directory_file_helper( new, self.custom_checker.name ) if self.custom_validator: - self.custom_validator.name = _problem_directory_file( + self.custom_validator.name = problem_directory_file_helper( new, self.custom_validator.name ) self.save() From fe5e7198ee9ce11ae442eaea3edcac391ab3d5d7 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 22 Sep 2022 17:33:14 -0500 Subject: [PATCH 4/9] Change lang stat page --- dmoj/urls.py | 22 +---- judge/views/stats.py | 148 ++++++++++++++++++---------------- templates/stats/base.html | 9 +-- templates/stats/language.html | 18 +---- templates/stats/tab.html | 5 ++ 5 files changed, 90 insertions(+), 112 deletions(-) create mode 100644 templates/stats/tab.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 68e691a..3468a19 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -886,27 +886,7 @@ urlpatterns = [ "^language/", include( [ - url("^$", stats.language, name="language_stats"), - url( - "^data/all/$", - stats.language_data, - name="language_stats_data_all", - ), - 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("^$", stats.StatLanguage.as_view(), name="language_stats"), ] ), ), diff --git a/judge/views/stats.py b/judge/views/stats.py index e8c74ef..640088e 100644 --- a/judge/views/stats.py +++ b/judge/views/stats.py @@ -1,5 +1,6 @@ from itertools import chain, repeat from operator import itemgetter +import json from django.conf import settings from django.db.models import Case, Count, FloatField, IntegerField, Value, When @@ -7,6 +8,9 @@ from django.db.models.expressions import CombinedExpression from django.http import JsonResponse from django.shortcuts import render 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 judge.models import Language, Submission from judge.utils.stats import ( @@ -17,86 +21,88 @@ from judge.utils.stats import ( ) -ac_count = Count( - Case(When(submission__result="AC", then=Value(1)), output_field=IntegerField()) -) +class StatViewBase(TemplateView): + def get(self, request, *args, **kwargs): + if not request.user.is_superuser: + raise Http404 + return super().get(request, *args, **kwargs) -def repeat_chain(iterable): - return chain.from_iterable(repeat(iterable)) - - -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( - { - "labels": list(map(itemgetter("name"), languages[:num_languages])) - + ["Other"], - "datasets": [ - { - "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"], - "highlightBackgroundColor": highlight_colors[:num_languages] - + ["#FFC870"], - "data": list(map(itemgetter("count"), languages[:num_languages])) - + [other_count], - }, - ], - }, - safe=False, +class StatLanguage(StatViewBase): + template_name = "stats/language.html" + ac_count = Count( + Case(When(submission__result="AC", then=Value(1)), output_field=IntegerField()) ) + def repeat_chain(iterable): + return chain.from_iterable(repeat(iterable)) -def ac_language_data(request): - return language_data(request, Language.objects.annotate(count=ac_count)) - - -def status_data(request, statuses=None): - if not statuses: - statuses = ( - Submission.objects.values("result") - .annotate(count=Count("result")) - .values("result", "count") + 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") ) - data = [] - for status in statuses: - res = status["result"] - if not res: - continue - count = status["count"] - data.append((str(Submission.USER_DISPLAY_CODES[res]), count)) + num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD) + other_count = sum(map(itemgetter("count"), languages[num_languages:])) - return JsonResponse(get_pie_chart(data), safe=False) + return { + "labels": list(map(itemgetter("name"), languages[:num_languages])) + + ["Other"], + "datasets": [ + { + "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"], + "highlightBackgroundColor": highlight_colors[:num_languages] + + ["#FFC870"], + "data": list(map(itemgetter("count"), languages[:num_languages])) + + [other_count], + }, + ], + } + + def ac_language_data(self): + return self.language_data(Language.objects.annotate(count=self.ac_count)) + + def status_data(self, statuses=None): + 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 ac_rate(request): - rate = CombinedExpression( - ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField() - ) - data = ( - Language.objects.annotate(total=Count("submission"), ac_rate=rate) - .filter(total__gt=0) - .order_by("total") - .values_list("name", "ac_rate") - ) - return JsonResponse(get_bar_chart(list(data))) + def ac_rate(self): + rate = CombinedExpression( + self.ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField() + ) + data = ( + Language.objects.annotate(total=Count("submission"), ac_rate=rate) + .filter(total__gt=0) + .order_by("total") + .values_list("name", "ac_rate") + ) + return get_bar_chart(list(data)) -def language(request): - return render( - request, - "stats/language.html", - { - "title": _("Language statistics"), - "tab": "language", - }, - ) + 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 \ No newline at end of file diff --git a/templates/stats/base.html b/templates/stats/base.html index d83ac96..6e72989 100644 --- a/templates/stats/base.html +++ b/templates/stats/base.html @@ -6,13 +6,10 @@ {% endcompress %} {% endblock %} -{% block content_title %}{{ _('Statistics') }}{% endblock %} +{% block content_title %}{% endblock %} +{% block title_ruler %}{% endblock %} {% block body %} - + {% include "stats/tab.html" %} {% block chart_body %}{% endblock %} {% endblock %} diff --git a/templates/stats/language.html b/templates/stats/language.html index d142c62..70ec589 100644 --- a/templates/stats/language.html +++ b/templates/stats/language.html @@ -36,20 +36,10 @@ Chart.defaults.global.tooltipFontFamily = Chart.defaults.global.tooltipTitleFontFamily = $('body').css('font-family'); - - function pie_chart(url, $chart) { - $.getJSON(url, function (data) { - draw_pie_chart(data, $chart); - }); - } - - 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')); - }); + draw_pie_chart({{ data_all }}, $('#lang-all')); + draw_pie_chart({{ lang_ac }}, $('#lang-ac')); + draw_pie_chart({{ status_counts }}, $('#status-counts')); + draw_bar_chart({{ ac_rate }}, $('#ac-rate')); }); {% endblock %} diff --git a/templates/stats/tab.html b/templates/stats/tab.html new file mode 100644 index 0000000..0b7a1d7 --- /dev/null +++ b/templates/stats/tab.html @@ -0,0 +1,5 @@ +{% extends "tabs-base.html" %} + +{% block tabs %} + {{ make_tab('language', 'fa-list', url('language_stats'), _('Language')) }} +{% endblock %} \ No newline at end of file From 3da6a258679bf1f8493f31cf9da6e4ffdea8bcdd Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 22 Sep 2022 19:23:41 -0500 Subject: [PATCH 5/9] Add stat page --- dmoj/urls.py | 14 +++- judge/models/problem_data.py | 4 +- judge/views/stats.py | 124 +++++++++++++++++++++++++++++----- requirements.txt | 3 +- templates/base.html | 1 + templates/stats/media-js.html | 20 ++++++ templates/stats/site.html | 65 ++++++++++++++++++ templates/stats/tab.html | 1 + 8 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 templates/stats/site.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 3468a19..255a951 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -886,7 +886,19 @@ urlpatterns = [ "^language/", include( [ - url("^$", stats.StatLanguage.as_view(), name="language_stats"), + url( + "^$", + stats.StatLanguage.as_view(), + name="language_stats", + ), + ] + ), + ), + url( + "^site/", + include( + [ + url("^$", stats.StatSite.as_view(), name="site_stats"), ] ), ), diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py index 1469c3f..1717924 100644 --- a/judge/models/problem_data.py +++ b/judge/models/problem_data.py @@ -154,7 +154,9 @@ class ProblemData(models.Model): if self.zipfile: self.zipfile.name = problem_directory_file_helper(new, self.zipfile.name) if self.generator: - self.generator.name = problem_directory_file_helper(new, self.generator.name) + self.generator.name = problem_directory_file_helper( + new, self.generator.name + ) if self.custom_checker: self.custom_checker.name = problem_directory_file_helper( new, self.custom_checker.name diff --git a/judge/views/stats.py b/judge/views/stats.py index 640088e..5071dd5 100644 --- a/judge/views/stats.py +++ b/judge/views/stats.py @@ -1,6 +1,7 @@ from itertools import chain, repeat from operator import itemgetter import json +import pandas as pd from django.conf import settings from django.db.models import Case, Count, FloatField, IntegerField, Value, When @@ -11,6 +12,7 @@ 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.utils.stats import ( @@ -49,18 +51,18 @@ class StatLanguage(StatViewBase): other_count = sum(map(itemgetter("count"), languages[num_languages:])) return { - "labels": list(map(itemgetter("name"), languages[:num_languages])) - + ["Other"], - "datasets": [ - { - "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"], - "highlightBackgroundColor": highlight_colors[:num_languages] - + ["#FFC870"], - "data": list(map(itemgetter("count"), languages[:num_languages])) - + [other_count], - }, - ], - } + "labels": list(map(itemgetter("name"), languages[:num_languages])) + + ["Other"], + "datasets": [ + { + "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"], + "highlightBackgroundColor": highlight_colors[:num_languages] + + ["#FFC870"], + "data": list(map(itemgetter("count"), languages[:num_languages])) + + [other_count], + }, + ], + } def ac_language_data(self): return self.language_data(Language.objects.annotate(count=self.ac_count)) @@ -83,10 +85,12 @@ class StatLanguage(StatViewBase): return get_pie_chart(data) - def ac_rate(self): rate = CombinedExpression( - self.ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField() + self.ac_count / Count("submission"), + "*", + Value(100.0), + output_field=FloatField(), ) data = ( Language.objects.annotate(total=Count("submission"), ac_rate=rate) @@ -96,7 +100,6 @@ class StatLanguage(StatViewBase): ) return get_bar_chart(list(data)) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = _("Language statistics") @@ -105,4 +108,93 @@ class StatLanguage(StatViewBase): 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 \ No newline at end of file + return context + + +def group_data_by_period(dates, data, period="1W"): + df = pd.DataFrame() + df["dates"] = pd.to_datetime(dates) + df["data"] = data + df = df.groupby([pd.Grouper(key="dates", freq=period)])["data"].mean().reset_index() + return list(df["dates"]), list(df["data"]) + + +class StatSite(StatViewBase): + template_name = "stats/site.html" + + def create_dataset(self, query, label): + labels = [] + data = [] + 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 diff --git a/requirements.txt b/requirements.txt index fdf6768..0743316 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,4 +37,5 @@ redis lupa websocket-client python-memcached -numpy \ No newline at end of file +numpy +pandas \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index b925c04..d9f86c8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -282,6 +282,7 @@ {% endif %} {% if request.user.is_superuser %}
  • {{ _('Internal') }}
  • +
  • {{ _('Stats') }}
  • {% endif %}
  • {{ _('Edit profile') }}
  • {% if request.user.is_impersonate %} diff --git a/templates/stats/media-js.html b/templates/stats/media-js.html index 69f9ad8..d6eb09d 100644 --- a/templates/stats/media-js.html +++ b/templates/stats/media-js.html @@ -87,4 +87,24 @@ }); 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; + } diff --git a/templates/stats/site.html b/templates/stats/site.html new file mode 100644 index 0000000..aa67a07 --- /dev/null +++ b/templates/stats/site.html @@ -0,0 +1,65 @@ +{% extends "stats/base.html" %} +{% block media %} + +{% endblock %} + +{% block chart_body %} +
    +
    + +
    +

    {{ _('Submissions') }}

    +
    +
    +
    + +
    +

    {{ _('Contests') }}

    +
    +
    +
    + +
    +

    {{ _('New users') }}

    +
    +
    +
    + +
    +

    {{ _('Groups') }}

    +
    +
    +
    + +
    +

    {{ _('Comments') }}

    +
    +
    +
    + +
    +

    {{ _('Chat messages') }}

    +
    +{% endblock %} + +{% block bodyend %} + +{% endblock %} diff --git a/templates/stats/tab.html b/templates/stats/tab.html index 0b7a1d7..92ede48 100644 --- a/templates/stats/tab.html +++ b/templates/stats/tab.html @@ -2,4 +2,5 @@ {% block tabs %} {{ make_tab('language', 'fa-list', url('language_stats'), _('Language')) }} + {{ make_tab('site', 'fa-list', url('site_stats'), _('Site')) }} {% endblock %} \ No newline at end of file From f36a5497e022576b85f9536fec6d95e4060d0a5a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 24 Sep 2022 23:50:26 -0500 Subject: [PATCH 6/9] Fix error when access deleted org --- judge/views/organization.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/judge/views/organization.py b/judge/views/organization.py index c251bc5..0414516 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -157,7 +157,9 @@ class OrganizationMixin(OrganizationBase): class AdminOrganizationMixin(OrganizationMixin): def dispatch(self, 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 generic_message( request, @@ -170,7 +172,7 @@ class AdminOrganizationMixin(OrganizationMixin): class MemberOrganizationMixin(OrganizationMixin): def dispatch(self, 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 generic_message( request, From 539a2639ee41b779f11de194498bc306e3da1dbe Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 29 Sep 2022 12:23:16 -0500 Subject: [PATCH 7/9] Allow volunteer search --- judge/views/problem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/judge/views/problem.py b/judge/views/problem.py index 9cf20e9..ba9790c 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -845,8 +845,10 @@ class ProblemFeed(ProblemList): if self.feed_type == "new": return queryset.order_by("-date") elif user and self.feed_type == "volunteer": - voted_problems = user.volunteer_problem_votes.values_list( - "problem", flat=True + voted_problems = ( + user.volunteer_problem_votes.values_list("problem", flat=True) + if not bool(self.search_query) + else [] ) if self.show_solved_only: queryset = queryset.filter( From d0a0f86d466259d5145ce9deff0e54d555ee4bd8 Mon Sep 17 00:00:00 2001 From: Zhao-Linux Date: Tue, 4 Oct 2022 07:18:35 +0700 Subject: [PATCH 8/9] Change about file --- judge/forms.py | 2 -- judge/views/organization.py | 2 +- templates/about/about.html | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/judge/forms.py b/judge/forms.py index f155d49..396315d 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -153,8 +153,6 @@ class EditOrganizationForm(ModelForm): widgets["about"] = HeavyPreviewPageDownWidget( preview=reverse_lazy("organization_preview") ) - - class AddOrganizationForm(ModelForm): class Meta: model = Organization diff --git a/judge/views/organization.py b/judge/views/organization.py index 0414516..da99a7d 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -390,7 +390,7 @@ class OrganizationContestMixin( ): model = Contest - def is_contest_editable(self, request, contest): + def is_contest_able(self, request, contest): return request.profile in contest.authors.all() or self.can_edit_organization( self.organization ) diff --git a/templates/about/about.html b/templates/about/about.html index 32fd390..1fc305e 100644 --- a/templates/about/about.html +++ b/templates/about/about.html @@ -5,6 +5,7 @@ LQDOJ (Le Quy Don Online Judge) 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ở DMOJ. Đượ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 trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng), 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 Github repo chính thức. Mọi ý kiến đóng góp và thắc mắc xin gửi về: From 9438fa3e148848db2accddc7be0d0c2e5740f09e Mon Sep 17 00:00:00 2001 From: Zhao-Linux Date: Tue, 4 Oct 2022 10:33:16 +0700 Subject: [PATCH 9/9] Change about.html file --- judge/forms.py | 2 ++ judge/views/organization.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/judge/forms.py b/judge/forms.py index 396315d..f155d49 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -153,6 +153,8 @@ class EditOrganizationForm(ModelForm): widgets["about"] = HeavyPreviewPageDownWidget( preview=reverse_lazy("organization_preview") ) + + class AddOrganizationForm(ModelForm): class Meta: model = Organization diff --git a/judge/views/organization.py b/judge/views/organization.py index da99a7d..0414516 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -390,7 +390,7 @@ class OrganizationContestMixin( ): model = Contest - def is_contest_able(self, request, contest): + def is_contest_editable(self, request, contest): return request.profile in contest.authors.all() or self.can_edit_organization( self.organization )