Cloned DMOJ

This commit is contained in:
thanhluong 2020-01-21 15:35:58 +09:00
parent f623974b58
commit 49dc9ff10c
513 changed files with 132349 additions and 39 deletions

36
judge/jinja2/__init__.py Normal file
View file

@ -0,0 +1,36 @@
import itertools
import json
from django.utils.http import urlquote
from jinja2.ext import Extension
from mptt.utils import get_cached_trees
from statici18n.templatetags.statici18n import inlinei18n
from judge.highlight_code import highlight_code
from judge.user_translations import gettext
from . import (camo, datetime, filesize, gravatar, language, markdown, rating, reference, render, social,
spaceless, submission, timedelta)
from . import registry
registry.function('str', str)
registry.filter('str', str)
registry.filter('json', json.dumps)
registry.filter('highlight', highlight_code)
registry.filter('urlquote', urlquote)
registry.filter('roundfloat', round)
registry.function('inlinei18n', inlinei18n)
registry.function('mptt_tree', get_cached_trees)
registry.function('user_trans', gettext)
@registry.function
def counter(start=1):
return itertools.count(start).__next__
class DMOJExtension(Extension):
def __init__(self, env):
super(DMOJExtension, self).__init__(env)
env.globals.update(registry.globals)
env.filters.update(registry.filters)
env.tests.update(registry.tests)

9
judge/jinja2/camo.py Normal file
View file

@ -0,0 +1,9 @@
from judge.utils.camo import client as camo_client
from . import registry
@registry.filter
def camo(url):
if camo_client is None:
return url
return camo_client.rewrite_url(url)

27
judge/jinja2/datetime.py Normal file
View file

@ -0,0 +1,27 @@
import functools
from django.template.defaultfilters import date, time
from django.templatetags.tz import localtime
from django.utils.translation import gettext as _
from . import registry
def localtime_wrapper(func):
@functools.wraps(func)
def wrapper(datetime, *args, **kwargs):
if getattr(datetime, 'convert_to_local_time', True):
datetime = localtime(datetime)
return func(datetime, *args, **kwargs)
return wrapper
registry.filter(localtime_wrapper(date))
registry.filter(localtime_wrapper(time))
@registry.function
@registry.render_with('widgets/relative-time.html')
def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')):
return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs}

36
judge/jinja2/filesize.py Normal file
View file

@ -0,0 +1,36 @@
from django.utils.html import avoid_wrapping
from . import registry
def _format_size(bytes, callback):
bytes = float(bytes)
KB = 1 << 10
MB = 1 << 20
GB = 1 << 30
TB = 1 << 40
PB = 1 << 50
if bytes < KB:
return callback('', bytes)
elif bytes < MB:
return callback('K', bytes / KB)
elif bytes < GB:
return callback('M', bytes / MB)
elif bytes < TB:
return callback('G', bytes / GB)
elif bytes < PB:
return callback('T', bytes / TB)
else:
return callback('P', bytes / PB)
@registry.filter
def kbdetailformat(bytes):
return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x)))
@registry.filter
def kbsimpleformat(kb):
return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B'))

25
judge/jinja2/gravatar.py Normal file
View file

@ -0,0 +1,25 @@
import hashlib
from django.contrib.auth.models import AbstractUser
from django.utils.http import urlencode
from judge.models import Profile
from judge.utils.unicode import utf8bytes
from . import registry
@registry.function
def gravatar(email, size=80, default=None):
if isinstance(email, Profile):
if default is None:
default = email.mute
email = email.user.email
elif isinstance(email, AbstractUser):
email = email.email
gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?'
args = {'d': 'identicon', 's': str(size)}
if default:
args['f'] = 'y'
gravatar_url += urlencode(args)
return gravatar_url

18
judge/jinja2/language.py Normal file
View file

@ -0,0 +1,18 @@
from django.utils import translation
from . import registry
@registry.function('language_info')
def get_language_info(language):
# ``language`` is either a language code string or a sequence
# with the language code as its first item
if len(language[0]) > 1:
return translation.get_language_info(language[0])
else:
return translation.get_language_info(str(language))
@registry.function('language_info_list')
def get_language_info_list(langs):
return [get_language_info(lang) for lang in langs]

View file

@ -0,0 +1,142 @@
import logging
import re
from html.parser import HTMLParser
from urllib.parse import urlparse
import mistune
from django.conf import settings
from jinja2 import Markup
from lxml import html
from lxml.etree import ParserError, XMLSyntaxError
from judge.highlight_code import highlight_code
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
from judge.utils.camo import client as camo_client
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
from .. import registry
logger = logging.getLogger('judge.html')
NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED
class CodeSafeInlineGrammar(mistune.InlineGrammar):
double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word**
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
class AwesomeInlineGrammar(MathInlineGrammar, CodeSafeInlineGrammar):
pass
class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer):
grammar_class = AwesomeInlineGrammar
class AwesomeRenderer(MathRenderer, mistune.Renderer):
def __init__(self, *args, **kwargs):
self.nofollow = kwargs.pop('nofollow', True)
self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None
self.parser = HTMLParser()
super(AwesomeRenderer, self).__init__(*args, **kwargs)
def _link_rel(self, href):
if href:
try:
url = urlparse(href)
except ValueError:
return ' rel="nofollow"'
else:
if url.netloc and url.netloc not in NOFOLLOW_WHITELIST:
return ' rel="nofollow"'
return ''
def autolink(self, link, is_email=False):
text = link = mistune.escape(link)
if is_email:
link = 'mailto:%s' % link
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
def table(self, header, body):
return (
'<table class="table">\n<thead>%s</thead>\n'
'<tbody>\n%s</tbody>\n</table>\n'
) % (header, body)
def link(self, link, title, text):
link = mistune.escape_link(link)
if not title:
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
title = mistune.escape(title, quote=True)
return '<a href="%s" title="%s"%s>%s</a>' % (link, title, self._link_rel(link), text)
def block_code(self, code, lang=None):
if not lang:
return '\n<pre><code>%s</code></pre>\n' % mistune.escape(code).rstrip()
return highlight_code(code, lang)
def block_html(self, html):
if self.texoid and html.startswith('<latex'):
attr = html[6:html.index('>')]
latex = html[html.index('>') + 1:html.rindex('<')]
latex = self.parser.unescape(latex)
result = self.texoid.get_result(latex)
if not result:
return '<pre>%s</pre>' % mistune.escape(latex, smart_amp=False)
elif 'error' not in result:
img = ('''<img src="%(svg)s" onerror="this.src='%(png)s';this.onerror=null"'''
'width="%(width)s" height="%(height)s"%(tail)s>') % {
'svg': result['svg'], 'png': result['png'],
'width': result['meta']['width'], 'height': result['meta']['height'],
'tail': ' /' if self.options.get('use_xhtml') else '',
}
style = ['max-width: 100%',
'height: %s' % result['meta']['height'],
'max-height: %s' % result['meta']['height'],
'width: %s' % result['meta']['height']]
if 'inline' in attr:
tag = 'span'
else:
tag = 'div'
style += ['text-align: center']
return '<%s style="%s">%s</%s>' % (tag, ';'.join(style), img, tag)
else:
return '<pre>%s</pre>' % mistune.escape(result['error'], smart_amp=False)
return super(AwesomeRenderer, self).block_html(html)
def header(self, text, level, *args, **kwargs):
return super(AwesomeRenderer, self).header(text, level + 2, *args, **kwargs)
@registry.filter
def markdown(value, style, math_engine=None, lazy_load=False):
styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE)
escape = styles.get('safe_mode', True)
nofollow = styles.get('nofollow', True)
texoid = TEXOID_ENABLED and styles.get('texoid', False)
math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False)
post_processors = []
if styles.get('use_camo', False) and camo_client is not None:
post_processors.append(camo_client.update_tree)
if lazy_load:
post_processors.append(lazy_load_processor)
renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid,
math=math and math_engine is not None, math_engine=math_engine)
markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer,
parse_block_html=1, parse_inline_html=1)
result = markdown(value)
if post_processors:
try:
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))
except (XMLSyntaxError, ParserError) as e:
if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
logger.exception('Failed to parse HTML string')
tree = html.Element('div')
for processor in post_processors:
processor(tree)
result = html.tostring(tree, encoding='unicode')
return Markup(result)

View file

@ -0,0 +1,20 @@
from copy import deepcopy
from django.templatetags.static import static
from lxml import html
def lazy_load(tree):
blank = static('blank.gif')
for img in tree.xpath('.//img'):
src = img.get('src', '')
if src.startswith('data') or '-math' in img.get('class', ''):
continue
noscript = html.Element('noscript')
copy = deepcopy(img)
copy.tail = ''
noscript.append(copy)
img.addprevious(noscript)
img.set('data-src', src)
img.set('src', blank)
img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil')

View file

@ -0,0 +1,65 @@
import re
import mistune
from judge.utils.mathoid import MathoidMathParser
mistune._pre_tags.append('latex')
class MathInlineGrammar(mistune.InlineGrammar):
block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL)
math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL)
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|\\[\[(]|https?://| {2,}\n|$)')
class MathInlineLexer(mistune.InlineLexer):
grammar_class = MathInlineGrammar
def __init__(self, *args, **kwargs):
self.default_rules = self.default_rules[:]
self.inline_html_rules = self.default_rules
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'math')
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'block_math')
super(MathInlineLexer, self).__init__(*args, **kwargs)
def output_block_math(self, m):
return self.renderer.block_math(m.group(1) or m.group(2))
def output_math(self, m):
return self.renderer.math(m.group(1) or m.group(2))
def output_inline_html(self, m):
tag = m.group(1)
text = m.group(3)
if self._parse_inline_html and text:
if tag == 'a':
self._in_link = True
text = self.output(text)
self._in_link = False
else:
text = self.output(text)
extra = m.group(2) or ''
html = '<%s%s>%s</%s>' % (tag, extra, text, tag)
else:
html = m.group(0)
return self.renderer.inline_html(html)
class MathRenderer(mistune.Renderer):
def __init__(self, *args, **kwargs):
if kwargs.pop('math', False):
self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg')
else:
self.mathoid = None
super(MathRenderer, self).__init__(*args, **kwargs)
def block_math(self, math):
if self.mathoid is None or not math:
return r'\[%s\]' % mistune.escape(str(math))
return self.mathoid.display_math(math)
def math(self, math):
if self.mathoid is None or not math:
return r'\(%s\)' % mistune.escape(str(math))
return self.mathoid.inline_math(math)

35
judge/jinja2/rating.py Normal file
View file

@ -0,0 +1,35 @@
from django.utils import six
from judge.ratings import rating_class, rating_name, rating_progress
from . import registry
def _get_rating_value(func, obj):
if obj is None:
return None
if isinstance(obj, six.integer_types):
return func(obj)
else:
return func(obj.rating)
@registry.function('rating_class')
def get_rating_class(obj):
return _get_rating_value(rating_class, obj) or 'rate-none'
@registry.function(name='rating_name')
def get_name(obj):
return _get_rating_value(rating_name, obj) or 'Unrated'
@registry.function(name='rating_progress')
def get_progress(obj):
return _get_rating_value(rating_progress, obj) or 0.0
@registry.function
@registry.render_with('user/rating.html')
def rating_number(obj):
return {'rating': obj}

187
judge/jinja2/reference.py Normal file
View file

@ -0,0 +1,187 @@
import re
from collections import defaultdict
from urllib.parse import urljoin
from ansi2html import Ansi2HTMLConverter
from django.contrib.auth.models import AbstractUser
from django.urls import reverse
from django.utils.safestring import mark_safe
from lxml.html import Element
from judge import lxml_tree
from judge.models import Contest, Problem, Profile
from judge.ratings import rating_class, rating_progress
from . import registry
rereference = re.compile(r'\[(r?user):(\w+)\]')
def get_user(username, data):
if not data:
element = Element('span')
element.text = username
return element
element = Element('span', {'class': Profile.get_user_css_class(*data)})
link = Element('a', {'href': reverse('user_page', args=[username])})
link.text = username
element.append(link)
return element
def get_user_rating(username, data):
if not data:
element = Element('span')
element.text = username
return element
rating = data[1]
element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])})
if rating:
rating_css = rating_class(rating)
rate_box = Element('span', {'class': 'rate-box ' + rating_css})
rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)}))
user = Element('span', {'class': 'rating ' + rating_css})
user.text = username
element.append(rate_box)
element.append(user)
else:
element.text = username
return element
def get_user_info(usernames):
return {name: (rank, rating) for name, rank, rating in
Profile.objects.filter(user__username__in=usernames)
.values_list('user__username', 'display_rank', 'rating')}
reference_map = {
'user': (get_user, get_user_info),
'ruser': (get_user_rating, get_user_info),
}
def process_reference(text):
# text/tail -> text/tail + elements
last = 0
tail = text
prev = None
elements = []
for piece in rereference.finditer(text):
if prev is None:
tail = text[last:piece.start()]
else:
prev.append(text[last:piece.start()])
prev = list(piece.groups())
elements.append(prev)
last = piece.end()
if prev is not None:
prev.append(text[last:])
return tail, elements
def populate_list(queries, list, element, tail, children):
if children:
for elem in children:
queries[elem[0]].append(elem[1])
list.append((element, tail, children))
def update_tree(list, results, is_tail=False):
for element, text, children in list:
after = []
for type, name, tail in children:
child = reference_map[type][0](name, results[type].get(name))
child.tail = tail
after.append(child)
after = iter(reversed(after))
if is_tail:
element.tail = text
link = element.getnext()
if link is None:
link = next(after)
element.getparent().append(link)
else:
element.text = text
link = next(after)
element.insert(0, link)
for child in after:
link.addprevious(child)
link = child
@registry.filter
def reference(text):
tree = lxml_tree.fromstring(text)
texts = []
tails = []
queries = defaultdict(list)
for element in tree.iter():
if element.text:
populate_list(queries, texts, element, *process_reference(element.text))
if element.tail:
populate_list(queries, tails, element, *process_reference(element.tail))
results = {type: reference_map[type][1](values) for type, values in queries.items()}
update_tree(texts, results, is_tail=False)
update_tree(tails, results, is_tail=True)
return tree
@registry.filter
def item_title(item):
if isinstance(item, Problem):
return item.name
elif isinstance(item, Contest):
return item.name
return '<Unknown>'
@registry.function
@registry.render_with('user/link.html')
def link_user(user):
if isinstance(user, Profile):
user, profile = user.user, user
elif isinstance(user, AbstractUser):
profile = user.profile
elif type(user).__name__ == 'ContestRankingProfile':
user, profile = user.user, user
else:
raise ValueError('Expected profile or user, got %s' % (type(user),))
return {'user': user, 'profile': profile}
@registry.function
@registry.render_with('user/link-list.html')
def link_users(users):
return {'users': users}
@registry.function
@registry.render_with('runtime-version-fragment.html')
def runtime_versions(versions):
return {'runtime_versions': versions}
@registry.filter(name='absolutify')
def absolute_links(text, url):
tree = lxml_tree.fromstring(text)
for anchor in tree.xpath('.//a'):
href = anchor.get('href')
if href:
anchor.set('href', urljoin(url, href))
return tree
@registry.function(name='urljoin')
def join(first, second, *rest):
if not rest:
return urljoin(first, second)
return urljoin(urljoin(first, second), *rest)
@registry.filter(name='ansi2html')
def ansi2html(s):
return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False))

53
judge/jinja2/registry.py Normal file
View file

@ -0,0 +1,53 @@
from django_jinja.library import render_with
globals = {}
tests = {}
filters = {}
extensions = []
__all__ = ['render_with', 'function', 'filter', 'test', 'extension']
def _store_function(store, func, name=None):
if name is None:
name = func.__name__
store[name] = func
def _register_function(store, name, func):
if name is None and func is None:
def decorator(func):
_store_function(store, func)
return func
return decorator
elif name is not None and func is None:
if callable(name):
_store_function(store, name)
return name
else:
def decorator(func):
_store_function(store, func, name)
return func
return decorator
else:
_store_function(store, func, name)
return func
def filter(name=None, func=None):
return _register_function(filters, name, func)
def function(name=None, func=None):
return _register_function(globals, name, func)
def test(name=None, func=None):
return _register_function(tests, name, func)
def extension(cls):
extensions.append(cls)
return cls

27
judge/jinja2/render.py Normal file
View file

@ -0,0 +1,27 @@
from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError,
VariableDoesNotExist)
from . import registry
MAX_CACHE = 100
django_cache = {}
def compile_template(code):
if code in django_cache:
return django_cache[code]
# If this works for re.compile, it works for us too.
if len(django_cache) > MAX_CACHE:
django_cache.clear()
t = django_cache[code] = DjangoTemplate(code)
return t
@registry.function
def render_django(template, **context):
try:
return compile_template(template).render(Context(context))
except (VariableDoesNotExist, DjangoTemplateSyntaxError):
return 'Error rendering: %r' % template

34
judge/jinja2/social.py Normal file
View file

@ -0,0 +1,34 @@
from django.template.loader import get_template
from django.utils.safestring import mark_safe
from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_gplus_url, post_to_twitter_url
from . import registry
SHARES = [
('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url),
('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url),
('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url),
# For future versions:
# ('post_to_linkedin', 'django_social_share/templatetags/post_to_linkedin.html', post_to_linkedin_url),
# ('post_to_reddit', 'django_social_share/templatetags/post_to_reddit.html', post_to_reddit_url),
]
def make_func(name, template, url_func):
def func(request, *args):
link_text = args[-1]
context = {'request': request, 'link_text': mark_safe(link_text)}
context = url_func(context, *args[:-1])
return mark_safe(get_template(template).render(context))
func.__name__ = name
registry.function(name, func)
for name, template, url_func in SHARES:
make_func(name, template, url_func)
@registry.function
def recaptcha_init(language=None):
return get_template('snowpenguin/recaptcha/recaptcha_init.html').render({'explicit': False, 'language': language})

29
judge/jinja2/spaceless.py Normal file
View file

@ -0,0 +1,29 @@
import re
from jinja2 import Markup, nodes
from jinja2.ext import Extension
class SpacelessExtension(Extension):
"""
Removes whitespace between HTML tags at compile time, including tab and newline characters.
It does not remove whitespace between jinja2 tags or variables. Neither does it remove whitespace between tags
and their text content.
Adapted from coffin:
https://github.com/coffin/coffin/blob/master/coffin/template/defaulttags.py
Adapted from StackOverflow:
https://stackoverflow.com/a/23741298/1090657
"""
tags = {'spaceless'}
def parse(self, parser):
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
return nodes.CallBlock(
self.call_method('_strip_spaces', [], [], None, None),
[], [], body,
).set_lineno(lineno)
def _strip_spaces(self, caller=None):
return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip()))

View file

@ -0,0 +1,21 @@
from . import registry
@registry.function
def submission_layout(submission, profile_id, user, editable_problem_ids, completed_problem_ids):
problem_id = submission.problem_id
can_view = False
if problem_id in editable_problem_ids:
can_view = True
if profile_id == submission.user_id:
can_view = True
if user.has_perm('judge.change_submission'):
can_view = True
if submission.problem_id in completed_problem_ids:
can_view |= submission.problem.is_public or profile_id in submission.problem.tester_ids
return can_view

28
judge/jinja2/timedelta.py Normal file
View file

@ -0,0 +1,28 @@
import datetime
from judge.utils.timedelta import nice_repr
from . import registry
@registry.filter
def timedelta(value, display='long'):
if value is None:
return value
return nice_repr(value, display)
@registry.filter
def timestampdelta(value, display='long'):
value = datetime.timedelta(seconds=value)
return timedelta(value, display)
@registry.filter
def seconds(timedelta):
return timedelta.total_seconds()
@registry.filter
@registry.render_with('time-remaining-fragment.html')
def as_countdown(timedelta):
return {'countdown': timedelta}