pull requirement.txt
This commit is contained in:
commit
c44fb6c47a
19 changed files with 479 additions and 263 deletions
30
dmoj/urls.py
30
dmoj/urls.py
|
@ -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"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
127
requirements.txt
127
requirements.txt
|
@ -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
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
88
templates/organization/contest/edit.html
Normal file
88
templates/organization/contest/edit.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
65
templates/stats/site.html
Normal 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
6
templates/stats/tab.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue