diff --git a/dmoj/settings.py b/dmoj/settings.py index 01b0f0be9..4fe766527 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -536,7 +536,7 @@ BLEACH_USER_SAFE_ATTRS = { '*': ['id', 'class', 'style', 'data', 'height'], - 'img': ['src', 'alt', 'title', 'width', 'height', 'data-src'], + 'img': ['src', 'alt', 'title', 'width', 'height', 'data-src', 'align'], 'a': ['href', 'alt', 'title'], 'iframe': ['src', 'height', 'width', 'allow'], 'abbr': ['title'], @@ -548,6 +548,7 @@ 'audio': ['autoplay', 'controls', 'crossorigin', 'muted', 'loop', 'preload', 'src'], 'video': ['autoplay', 'controls', 'crossorigin', 'height', 'muted', 'loop', 'poster', 'preload', 'src', 'width'], 'source': ['src', 'srcset', 'type'], + 'li': ['value'], } MARKDOWN_STAFF_EDITABLE_STYLE = { diff --git a/judge/forms.py b/judge/forms.py index 154c30d52..d2e5daca0 100755 --- a/judge/forms.py +++ b/judge/forms.py @@ -460,7 +460,7 @@ def widget_attrs(self, widget): class TOTPForm(Form): TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES - totp_or_scratch_code = NoAutoCompleteCharField(required=False) + totp_or_scratch_code = NoAutoCompleteCharField(required=False, widget=forms.TextInput(attrs={'autofocus': True})) def __init__(self, *args, **kwargs): self.profile = kwargs.pop('profile') diff --git a/judge/management/commands/import_polygon_package.py b/judge/management/commands/import_polygon_package.py index 95beca95c..8dfb07630 100644 --- a/judge/management/commands/import_polygon_package.py +++ b/judge/management/commands/import_polygon_package.py @@ -11,6 +11,7 @@ from django.conf import settings from django.contrib.sites.models import Site from django.core.files import File +from django.core.files.storage import default_storage from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.urls import reverse @@ -255,23 +256,39 @@ def parse_statements(problem_meta, root, package): problem_meta['translations'] = [] problem_meta['tutorial'] = '' - image_cache = {} + def process_images(text): + image_cache = problem_meta['image_cache'] - def save_image(image_path): - norm_path = os.path.normpath(os.path.join(statement_folder, image_path)) - sha1 = hashlib.sha1() - sha1.update(package.open(norm_path, 'r').read()) - sha1 = sha1.hexdigest() + def save_image(image_path): + norm_path = os.path.normpath(os.path.join(statement_folder, image_path)) + sha1 = hashlib.sha1() + sha1.update(package.open(norm_path, 'r').read()) + sha1 = sha1.hexdigest() - if sha1 not in image_cache: - image = File( - file=package.open(norm_path, 'r'), - name=os.path.basename(image_path), + if sha1 not in image_cache: + image = File( + file=package.open(norm_path, 'r'), + name=os.path.basename(image_path), + ) + data = json.loads(django_uploader(image)) + image_cache[sha1] = data['link'] + + return image_cache[sha1] + + for image_path in set(re.findall(r'!\[image\]\((.+?)\)', text)): + text = text.replace( + f'', + f'})', ) - data = json.loads(django_uploader(image)) - image_cache[sha1] = data['link'] - return image_cache[sha1] + for img_tag in set(re.findall(r'<\s*img[^>]*>', text)): + image_path = re.search(r'<\s*img[^>]+src\s*=\s*(["\'])(.*?)\1[^>]*>', text).group(2) + text = text.replace( + img_tag, + img_tag.replace(image_path, save_image(image_path)), + ) + + return text def parse_problem_properties(problem_properties): description = '' @@ -304,20 +321,6 @@ def parse_problem_properties(problem_properties): description += '\n## Notes\n\n' description += pandoc_tex_to_markdown(problem_properties['notes']) - # Images - for image_path in set(re.findall(r'!\[image\]\((.+?)\)', description)): - description = description.replace( - f'', - f'})', - ) - - for img_tag in set(re.findall(r'<\s*img[^>]*>', description)): - image_path = re.search(r'<\s*img[^>]+src\s*=\s*(["\'])(.*?)\1[^>]*>', description).group(2) - description = description.replace( - img_tag, - img_tag.replace(image_path, save_image(image_path)), - ) - return description def input_choice(prompt, choices): @@ -351,7 +354,7 @@ def input_choice(prompt, choices): description = parse_problem_properties(problem_properties) translations.append({ 'language': language, - 'description': description, + 'description': process_images(description), }) tutorial = problem_properties['tutorial'] @@ -378,6 +381,9 @@ def input_choice(prompt, choices): elif len(tutorials) > 0: problem_meta['tutorial'] = tutorials[0]['tutorial'] + # Process images for only the selected tutorial + problem_meta['tutorial'] = process_images(problem_meta['tutorial']) + for t in translations: language = t['language'] description = t['description'] @@ -545,6 +551,7 @@ def handle(self, *args, **options): # A dictionary to hold all problem information. problem_meta = {} + problem_meta['image_cache'] = {} problem_meta['code'] = problem_code problem_meta['tmp_dir'] = tempfile.TemporaryDirectory() problem_meta['authors'] = problem_authors @@ -555,6 +562,11 @@ def handle(self, *args, **options): parse_statements(problem_meta, root, package) create_problem(problem_meta) except Exception: + # Remove imported images + for image_url in problem_meta['image_cache'].values(): + path = default_storage.path(os.path.join(settings.MARTOR_UPLOAD_MEDIA_DIR, os.path.basename(image_url))) + os.remove(path) + raise finally: problem_meta['tmp_dir'].cleanup() diff --git a/judge/views/user.py b/judge/views/user.py index 7f56a4278..9bcc56319 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -23,6 +23,7 @@ from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.formats import date_format from django.utils.functional import cached_property from django.utils.safestring import mark_safe @@ -286,9 +287,23 @@ def get_context_data(self, **kwargs): return context + @method_decorator(require_POST) + def delete_comments(self, request, *args, **kwargs): + if not request.user.is_superuser: + raise PermissionDenied() + + user_id = User.objects.get(username=kwargs['user']).id + user = Profile.objects.get(user=user_id) + for comment in Comment.get_newest_visible_comments(viewer=request.user, author=user, + batch=2 * self.paginate_by): + comment.get_descendants(include_self=True).update(hidden=True) + return HttpResponseRedirect(reverse('user_comment', args=(user.user.username,))) + def dispatch(self, request, *args, **kwargs): if not self.request.user.is_superuser: raise PermissionDenied() + if request.method == 'POST': + return self.delete_comments(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs) @@ -535,7 +550,7 @@ class UserList(QueryStringSortMixin, InfinitePaginationMixin, DiggPaginatorMixin paginate_by = 100 all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points')) default_desc = all_sorts - default_sort = '-performance_points' + default_sort = '-rating' def get_queryset(self): return (Profile.objects.filter(is_unlisted=False).order_by(self.order) diff --git a/requirements.txt b/requirements.txt index 3abb8a165..7fafbc2d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,8 @@ dmoj-wpadmin @ git+https://github.com/DMOJ/dmoj-wpadmin.git lxml Pygments<2.12 mistune<2 -social-auth-app-django +social-auth-core==4.3.0 +social-auth-app-django==5.0.0 pytz django-statici18n pika diff --git a/templates/user/comment.html b/templates/user/comment.html index 106246ca2..a23124809 100644 --- a/templates/user/comment.html +++ b/templates/user/comment.html @@ -13,6 +13,10 @@ {% block body %} {% block before_comments %}{% endblock %} +