Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
36
judge/jinja2/__init__.py
Normal file
36
judge/jinja2/__init__.py
Normal 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
9
judge/jinja2/camo.py
Normal 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
27
judge/jinja2/datetime.py
Normal 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
36
judge/jinja2/filesize.py
Normal 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
25
judge/jinja2/gravatar.py
Normal 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
18
judge/jinja2/language.py
Normal 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]
|
142
judge/jinja2/markdown/__init__.py
Normal file
142
judge/jinja2/markdown/__init__.py
Normal 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)
|
20
judge/jinja2/markdown/lazy_load.py
Normal file
20
judge/jinja2/markdown/lazy_load.py
Normal 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')
|
65
judge/jinja2/markdown/math.py
Normal file
65
judge/jinja2/markdown/math.py
Normal 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
35
judge/jinja2/rating.py
Normal 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
187
judge/jinja2/reference.py
Normal 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
53
judge/jinja2/registry.py
Normal 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
27
judge/jinja2/render.py
Normal 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
34
judge/jinja2/social.py
Normal 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
29
judge/jinja2/spaceless.py
Normal 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()))
|
21
judge/jinja2/submission.py
Normal file
21
judge/jinja2/submission.py
Normal 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
28
judge/jinja2/timedelta.py
Normal 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}
|
Loading…
Add table
Add a link
Reference in a new issue