From 5cf986b19f12faba6d27897ac28d9aec7a973f0b Mon Sep 17 00:00:00 2001 From: Tudor Brindus Date: Mon, 6 Mar 2023 08:57:18 -0500 Subject: [PATCH] Simplify PDF rendering backend - Drop Puppeteer and Selenium backends - "Inline" pdfoid, and move `pdf_problems.py` to `utils` + rename it `pdfoid.py` - Drop a lot of pointless error checking; if we fail we'll want to raise a 500 regardless Co-authored-by: Quantum --- dmoj/settings.py | 41 ++-- judge/management/commands/render_pdf.py | 39 +--- judge/pdf_problems.py | 292 ------------------------ judge/signals.py | 10 +- judge/utils/pdfoid.py | 44 ++++ judge/views/problem.py | 75 +++--- templates/problem/raw.html | 18 +- 7 files changed, 133 insertions(+), 386 deletions(-) delete mode 100644 judge/pdf_problems.py create mode 100644 judge/utils/pdfoid.py diff --git a/dmoj/settings.py b/dmoj/settings.py index cf203ac8b..345cc7525 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -11,7 +11,6 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import datetime import os -import tempfile from django.utils.translation import gettext_lazy as _ from django_jinja.builtins import DEFAULT_EXTENSIONS @@ -165,8 +164,6 @@ SITE_FULL_URL = None # ie 'https://oj.vnoi.info', please remove the last / if needed -NODEJS = '/usr/bin/node' -EXIFTOOL = '/usr/bin/exiftool' ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3' SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' SELECT2_CSS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' @@ -175,41 +172,54 @@ DMOJ_CAMO_KEY = None DMOJ_CAMO_HTTPS = False DMOJ_CAMO_EXCLUDE = () + DMOJ_PROBLEM_DATA_ROOT = None + DMOJ_PROBLEM_MIN_TIME_LIMIT = 0 # seconds DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0 DMOJ_PROBLEM_HOT_PROBLEM_COUNT = 7 + DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS = {'“', '”', '‘', '’', '−', 'ff', 'fi', 'fl', 'ffi', 'ffl'} DMOJ_RATING_COLORS = True DMOJ_EMAIL_THROTTLING = (10, 60) VNOJ_DISCORD_WEBHOOK_THROTTLING = (10, 60) # Max 10 messages in 60 seconds -DMOJ_STATS_LANGUAGE_THRESHOLD = 10 -DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10 + # Maximum number of submissions a single user can queue without the `spam_submission` permission DMOJ_SUBMISSION_LIMIT = 2 +DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10 + # Whether to allow users to view source code: 'all' | 'all-solved' | 'only-own' DMOJ_SUBMISSION_SOURCE_VISIBILITY = 'all-solved' DMOJ_BLOG_NEW_PROBLEM_COUNT = 7 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_SCRATCH_CODES_COUNT = 5 DMOJ_USER_MAX_ORGANIZATION_COUNT = 3 + # Whether to allow users to download their data DMOJ_USER_DATA_DOWNLOAD = False DMOJ_USER_DATA_CACHE = '' DMOJ_USER_DATA_INTERNAL = '' DMOJ_USER_DATA_DOWNLOAD_RATELIMIT = datetime.timedelta(days=1) + # Whether to allow contest authors to download contest data DMOJ_CONTEST_DATA_DOWNLOAD = False DMOJ_CONTEST_DATA_CACHE = '' DMOJ_CONTEST_DATA_INTERNAL = '' DMOJ_CONTEST_DATA_DOWNLOAD_RATELIMIT = datetime.timedelta(days=1) + DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 DMOJ_COMMENT_REPLY_TIMEFRAME = datetime.timedelta(days=365) -DMOJ_PDF_PROBLEM_CACHE = '' -DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir() + +DMOJ_PDF_PDFOID_URL = None +# Optional but recommended to save resources, path on disk to cache PDFs +DMOJ_PDF_PROBLEM_CACHE = None +# Optional, URL serving DMOJ_PDF_PROBLEM_CACHE with X-Accel-Redirect +DMOJ_PDF_PROBLEM_INTERNAL = None + +DMOJ_STATS_LANGUAGE_THRESHOLD = 10 DMOJ_STATS_SUBMISSION_RESULT_COLORS = { 'TLE': '#a3bcbd', 'AC': '#00a92a', @@ -260,16 +270,6 @@ TERMS_OF_SERVICE_URL = None DEFAULT_USER_LANGUAGE = 'CPP17' -PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer' -PUPPETEER_PAPER_SIZE = 'Letter' - -USE_SELENIUM = False -SELENIUM_CUSTOM_CHROME_PATH = None -SELENIUM_CHROMEDRIVER_PATH = 'chromedriver' - -USE_PDFOID = False -PDFOID_URL = '' - INLINE_JQUERY = True INLINE_FONTAWESOME = True JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js' @@ -725,6 +725,11 @@ except IOError: pass -# Compute these values after local_settings.py is loaded +if DMOJ_PDF_PDFOID_URL: + # If a cache is configured, it must already exist and be a directory + assert DMOJ_PDF_PROBLEM_CACHE is None or os.path.isdir(DMOJ_PDF_PROBLEM_CACHE) + # If using X-Accel-Redirect, the cache directory must be configured + assert DMOJ_PDF_PROBLEM_INTERNAL is None or DMOJ_PDF_PROBLEM_CACHE is not None + ACE_DEFAULT_LIGHT_THEME = DMOJ_THEME_DEFAULT_ACE_THEME['light'] ACE_DEFAULT_DARK_THEME = DMOJ_THEME_DEFAULT_ACE_THEME['dark'] diff --git a/judge/management/commands/render_pdf.py b/judge/management/commands/render_pdf.py index a5b8e89b9..7f34d36f4 100644 --- a/judge/management/commands/render_pdf.py +++ b/judge/management/commands/render_pdf.py @@ -1,14 +1,10 @@ -import os -import shutil -import sys - from django.conf import settings from django.core.management.base import BaseCommand from django.template.loader import get_template from django.utils import translation from judge.models import Problem, ProblemTranslation -from judge.pdf_problems import DefaultPdfMaker, PdfoidPDFRender, PuppeteerPDFRender, SeleniumPDFRender +from judge.utils.pdfoid import render_pdf class Command(BaseCommand): @@ -16,13 +12,8 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('code', help='code of problem to render') - parser.add_argument('directory', nargs='?', help='directory to store temporaries') parser.add_argument('-l', '--language', default=settings.LANGUAGE_CODE, help='language to render PDF in') - parser.add_argument('-c', '--chrome', '--puppeteer', action='store_const', - const=PuppeteerPDFRender, default=DefaultPdfMaker, dest='engine') - parser.add_argument('-S', '--selenium', action='store_const', const=SeleniumPDFRender, dest='engine') - parser.add_argument('-p', '--pdfoid', action='store_const', const=PdfoidPDFRender, dest='engine') def handle(self, *args, **options): try: @@ -36,22 +27,14 @@ def handle(self, *args, **options): except ProblemTranslation.DoesNotExist: trans = None - directory = options['directory'] - with options['engine'](directory, clean_up=directory is None) as maker, \ - translation.override(options['language']): + with open(problem.code + '.pdf', 'wb') as f, translation.override(options['language']): problem_name = problem.name if trans is None else trans.name - maker.html = get_template('problem/raw.html').render({ - 'problem': problem, - 'problem_name': problem_name, - 'description': problem.description if trans is None else trans.description, - 'url': '', - 'math_engine': maker.math_engine, - }).replace('"//', '"https://').replace("'//", "'https://") - maker.title = problem_name - for file in ('style.css', 'mathjax_config.js'): - maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) - maker.make(debug=True) - if not maker.success: - print(maker.log, file=sys.stderr) - elif directory is None: - shutil.move(maker.pdffile, problem.code + '.pdf') + f.write(render_pdf( + html=get_template('problem/raw.html').render({ + 'problem': problem, + 'problem_name': problem_name, + 'description': problem.description if trans is None else trans.description, + 'url': '', + }).replace('"//', '"https://').replace("'//", "'https://"), + title=problem_name, + )) diff --git a/judge/pdf_problems.py b/judge/pdf_problems.py deleted file mode 100644 index 6c69abb3a..000000000 --- a/judge/pdf_problems.py +++ /dev/null @@ -1,292 +0,0 @@ -import base64 -import errno -import io -import json -import logging -import os -import shutil -import subprocess -import uuid -from base64 import b64decode - -import requests -from django.conf import settings -from django.utils.translation import gettext - -logger = logging.getLogger('judge.problem.pdf') - -HAS_SELENIUM = False -if settings.USE_SELENIUM: - try: - from selenium import webdriver - from selenium.common.exceptions import TimeoutException - from selenium.webdriver.common.by import By - from selenium.webdriver.support import expected_conditions as EC - from selenium.webdriver.support.ui import WebDriverWait - HAS_SELENIUM = True - except ImportError: - logger.warning('Failed to import Selenium', exc_info=True) - -NODE_PATH = settings.NODEJS -PUPPETEER_MODULE = settings.PUPPETEER_MODULE -HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE) - -PDFOID_URL = settings.PDFOID_URL -HAS_PDFOID = settings.USE_PDFOID and PDFOID_URL - -HAS_PDF = (os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and - (HAS_PDFOID or HAS_PUPPETEER or HAS_SELENIUM)) - -EXIFTOOL = settings.EXIFTOOL -HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK) - - -class BasePdfMaker(object): - math_engine = 'jax' - title = None - - def __init__(self, dir=None, clean_up=True, footer=True): - self.dir = dir or os.path.join(settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1())) - self.proc = None - self.log = None - self.htmlfile = os.path.join(self.dir, 'input.html') - self.pdffile = os.path.join(self.dir, 'output.pdf') - self.clean_up = clean_up - self.footer = footer - - def make(self, debug=False): - self._make(debug) - - if self.title and HAS_EXIFTOOL: - try: - subprocess.check_output([EXIFTOOL, '-Title=%s' % (self.title,), self.pdffile]) - except subprocess.CalledProcessError as e: - logger.error('Failed to run exiftool to set title for: %s\n%s', self.title, e.output) - - def _make(self, debug): - raise NotImplementedError() - - @property - def html(self): - with io.open(self.htmlfile, encoding='utf-8') as f: - return f.read() - - @html.setter - def html(self, data): - with io.open(self.htmlfile, 'w', encoding='utf-8') as f: - f.write(data) - - @property - def success(self): - return self.proc.returncode == 0 - - @property - def created(self): - return os.path.exists(self.pdffile) - - def __enter__(self): - try: - os.makedirs(self.dir) - except OSError as e: - if e.errno != errno.EEXIST: - raise - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self.clean_up: - shutil.rmtree(self.dir, ignore_errors=True) - - -class PuppeteerPDFRender(BasePdfMaker): - template = """\ -"use strict"; -const param = {params}; -const puppeteer = require('puppeteer'); - -puppeteer.launch().then(browser => Promise.resolve() - .then(async () => { - const page = await browser.newPage(); - await page.goto(param.input, { waitUntil: 'networkidle0' }); - await page.waitForSelector('.math-loaded', { timeout: 15000 }); - await page.pdf({ - path: param.output, - format: param.paper, - margin: { - top: '1cm', - bottom: '1cm', - left: '1cm', - right: '1cm', - }, - printBackground: true, - displayHeaderFooter: true, - headerTemplate: '
', - footerTemplate: param.footer ? '
' + - param.footer.replace('[page]', '') - .replace('[topage]', '') - + '
' : '
', - }); - await browser.close(); - }) - .catch(e => browser.close().then(() => {throw e})) -).catch(e => { - console.error(e); - process.exit(1); -}); -""" - - def get_render_script(self): - return self.template.replace('{params}', json.dumps({ - 'input': 'file://%s' % self.htmlfile, - 'output': self.pdffile, - 'paper': settings.PUPPETEER_PAPER_SIZE, - 'footer': gettext('Page [page] of [topage]') if self.footer else '', - })) - - def _make(self, debug): - with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f: - f.write(self.get_render_script()) - - env = os.environ.copy() - env['NODE_PATH'] = os.path.dirname(PUPPETEER_MODULE) - - cmdline = [NODE_PATH, '_render.js'] - self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env) - self.log = self.proc.communicate()[0] - - -class SeleniumPDFRender(BasePdfMaker): - success = False - template = { - 'printBackground': True, - 'displayHeaderFooter': True, - 'headerTemplate': '
', - 'footerTemplate': '
' + - gettext('Page %(current)s of %(total)s') % ({ - 'current': '', - 'total': '', - }) + '
', - } - - def get_log(self, driver): - return '\n'.join(map(str, driver.get_log('driver') + driver.get_log('browser'))) - - def _make(self, debug): - options = webdriver.ChromeOptions() - options.add_argument('--headless') - options.binary_location = settings.SELENIUM_CUSTOM_CHROME_PATH - - browser = webdriver.Chrome(settings.SELENIUM_CHROMEDRIVER_PATH, options=options) - browser.get('file://%s' % self.htmlfile) - self.log = self.get_log(browser) - - try: - WebDriverWait(browser, 15).until(EC.presence_of_element_located((By.CLASS_NAME, 'math-loaded'))) - except TimeoutException: - logger.error('PDF math rendering timed out') - self.log = self.get_log(browser) + '\nPDF math rendering timed out' - return - - template = self.template - if not self.footer: - template = template.copy() - template['footerTemplate'] = '
' - response = browser.execute_cdp_cmd('Page.printToPDF', template) - self.log = self.get_log(browser) - if not response: - return - - with open(self.pdffile, 'wb') as f: - f.write(base64.b64decode(response['data'])) - - self.success = True - - -# TODO(tbrindus): this class intentionally duplicates parts of DefaultPdfMaker, as it intends to -# entirely replace it once pdfoid is functional. -class PdfoidPDFRender(object): - # TODO(tbrindus): temporarily needed to keep judge/views/problem.py happy. - math_engine = 'jax' - wait_for_class = 'math-loaded' - wait_for_duration_secs = 15 - - @property - def footer_template(self): - return ('
' + - gettext('Page {page_number} of {total_pages}') + - '
') - - def __init__(self, dir=None, clean_up=True, footer=True): - self.html = None - self.title = None - self.log = None - self.success = False - self.clean_up = clean_up - self.footer = footer - self.dir = dir or os.path.join(settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1())) - self.pdffile = os.path.join(self.dir, 'output.pdf') - - def make(self, debug=False): - try: - assert self.html is not None - assert self.title is not None - - response = requests.post( - PDFOID_URL, - data={ - 'html': self.html, - 'title': self.title, - 'footer-template': self.footer_template if self.footer else None, - 'wait-for-class': self.wait_for_class, - 'wait-for-duration-secs': str(self.wait_for_duration_secs), - }, - ) - response.raise_for_status() - except requests.HTTPError as e: - if e.response.status_code == 400: - logger.error('pdfoid failed to render: %s', e.response.text) - else: - logger.exception('Failed to connect to pdfoid') - except Exception: - logger.exception('Failed to connect to pdfoid') - return - - try: - data = response.json() - except ValueError: - logger.exception('Invalid pdfoid response: %s', response.text) - - self.success = data['success'] - if not self.success: - self.log = data['error'] - else: - with open(self.pdffile, 'wb') as pdffile: - pdffile.write(b64decode(data['pdf'])) - - def load(self, _name, _path): - pass - - @property - def created(self): - return os.path.exists(self.pdffile) - - def __enter__(self): - try: - os.makedirs(self.dir) - except OSError as e: - if e.errno != errno.EEXIST: - raise - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self.clean_up: - shutil.rmtree(self.dir, ignore_errors=True) - - -if HAS_PDFOID: - DefaultPdfMaker = PdfoidPDFRender -elif HAS_PUPPETEER: - DefaultPdfMaker = PuppeteerPDFRender -elif HAS_SELENIUM: - DefaultPdfMaker = SeleniumPDFRender -else: - DefaultPdfMaker = None diff --git a/judge/signals.py b/judge/signals.py index 09a614a2b..87c2f8386 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -1,5 +1,6 @@ import errno import os +from typing import Optional from django.conf import settings from django.contrib.flatpages.models import FlatPage @@ -18,7 +19,10 @@ from judge.views.register import RegistrationView -def get_pdf_path(basename): +def get_pdf_path(basename: str) -> Optional[str]: + if not settings.DMOJ_PDF_PROBLEM_CACHE: + return None + return os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, basename) @@ -47,7 +51,9 @@ def problem_update(sender, instance, **kwargs): cache.delete_many(['generated-meta-problem:%s:%d' % (lang, instance.id) for lang, _ in settings.LANGUAGES]) for lang, _ in settings.LANGUAGES: - unlink_if_exists(get_pdf_path('%s.%s.pdf' % (instance.code, lang))) + cached_pdf_filename = get_pdf_path('%s.%s.pdf' % (instance.code, lang)) + if cached_pdf_filename is not None: + unlink_if_exists(cached_pdf_filename) @receiver(post_save, sender=Profile) diff --git a/judge/utils/pdfoid.py b/judge/utils/pdfoid.py new file mode 100644 index 000000000..9fd6fc3c0 --- /dev/null +++ b/judge/utils/pdfoid.py @@ -0,0 +1,44 @@ +import base64 +import logging + +import requests +from django.conf import settings +from django.utils.translation import gettext + +logger = logging.getLogger('judge.problem.pdf') + + +PDFOID_URL = settings.DMOJ_PDF_PDFOID_URL +PDF_RENDERING_ENABLED = PDFOID_URL is not None + + +def render_pdf(*, title: str, html: str, footer: bool = False) -> bytes: + if not PDF_RENDERING_ENABLED: + raise RuntimeError("pdfoid is not configured, can't render PDFs") + + if footer: + footer_template = ( + '
' + + gettext('Page {page_number} of {total_pages}') + + '
') + else: + footer_template = None + + response = requests.post( + PDFOID_URL, + data={ + 'html': html, + 'title': title, + 'footer-template': footer_template, + 'wait-for-class': 'math-loaded', + 'wait-for-duration-secs': 15, + }, + ) + + response.raise_for_status() + data = response.json() + + if not data['success']: + raise RuntimeError(data['error']) + + return base64.b64decode(data['pdf']) diff --git a/judge/views/problem.py b/judge/views/problem.py index 0c8af5794..ed82047e0 100755 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -1,7 +1,6 @@ import logging import os import re -import shutil import zipfile from datetime import timedelta from operator import itemgetter @@ -32,11 +31,11 @@ ProposeProblemSolutionFormSet from judge.models import ContestSubmission, Judge, Language, Problem, ProblemGroup, \ ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource -from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.tasks import on_new_problem from judge.template_context import misc_config from judge.utils.diggpaginator import DiggPaginator from judge.utils.opengraph import generate_opengraph +from judge.utils.pdfoid import PDF_RENDERING_ENABLED, render_pdf from judge.utils.problems import hot_problems, user_attempted_ids, \ user_completed_ids from judge.utils.strings import safe_float_or_none, safe_int_or_none @@ -208,7 +207,7 @@ def get_context_data(self, **kwargs): context['available_judges'] = Judge.objects.filter(online=True, problems=self.object) context['show_languages'] = self.object.allowed_languages.count() != Language.objects.count() - context['has_pdf_render'] = HAS_PDF + context['has_pdf_render'] = PDF_RENDERING_ENABLED context['completed_problem_ids'] = self.get_completed_problems() context['attempted_problems'] = self.get_attempted_problems() @@ -255,7 +254,7 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View): languages = set(map(itemgetter(0), settings.LANGUAGES)) def get(self, request, *args, **kwargs): - if not HAS_PDF: + if not PDF_RENDERING_ENABLED: raise Http404() language = kwargs.get('language', self.request.LANGUAGE_CODE) @@ -263,43 +262,47 @@ def get(self, request, *args, **kwargs): raise Http404() problem = self.get_object() - try: - trans = problem.translations.get(language=language) - except ProblemTranslation.DoesNotExist: - trans = None - - cache = os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, '%s.%s.pdf' % (problem.code, language)) - - if not os.path.exists(cache): - self.logger.info('Rendering: %s.%s.pdf', problem.code, language) - with DefaultPdfMaker() as maker, translation.override(language): - problem_name = problem.name if trans is None else trans.name - maker.html = get_template('problem/raw.html').render({ - 'problem': problem, - 'problem_name': problem_name, - 'description': problem.description if trans is None else trans.description, - 'url': request.build_absolute_uri(), - 'math_engine': maker.math_engine, - }).replace('"//', '"https://').replace("'//", "'https://") - maker.title = problem_name - - maker.make() - if not maker.success: - self.logger.error('Failed to render PDF for %s', problem.code) - return HttpResponse(maker.log, status=500, content_type='text/plain') - shutil.move(maker.pdffile, cache) + pdf_basename = '%s.%s.pdf' % (problem.code, language) + + def render_problem_pdf(): + self.logger.info('Rendering PDF in %s: %s', language, problem.code) + + with translation.override(language): + try: + trans = problem.translations.get(language=language) + except ProblemTranslation.DoesNotExist: + trans = None + + problem_name = trans.problem_name if trans else problem.name + return render_pdf( + html=get_template('problem/raw.html').render({ + 'problem': problem, + 'problem_name': problem_name, + 'description': trans.description if trans else problem.description, + 'url': request.build_absolute_uri(), + }).replace('"//', '"https://').replace("'//", "'https://"), + title=problem_name, + ) response = HttpResponse() + response['Content-Type'] = 'application/pdf' + response['Content-Disposition'] = f'inline; filename={pdf_basename}' - if hasattr(settings, 'DMOJ_PDF_PROBLEM_INTERNAL'): - url_path = '%s/%s.%s.pdf' % (settings.DMOJ_PDF_PROBLEM_INTERNAL, problem.code, language) - else: - url_path = None + if settings.DMOJ_PDF_PROBLEM_CACHE: + pdf_filename = os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, pdf_basename) + if not os.path.exists(pdf_filename): + with open(pdf_filename, 'wb') as f: + f.write(render_problem_pdf()) - add_file_response(request, response, url_path, cache) + if settings.DMOJ_PDF_PROBLEM_INTERNAL: + url_path = f'{settings.DMOJ_PDF_PROBLEM_INTERNAL}/{pdf_basename}' + else: + url_path = None + + add_file_response(request, response, url_path, pdf_filename) + else: + response.content = render_problem_pdf() - response['Content-Type'] = 'application/pdf' - response['Content-Disposition'] = 'inline; filename=%s.%s.pdf' % (problem.code, language) return response diff --git a/templates/problem/raw.html b/templates/problem/raw.html index f176858d7..ab9387202 100644 --- a/templates/problem/raw.html +++ b/templates/problem/raw.html @@ -79,16 +79,14 @@

{{ problem_name }}


- {{ description|markdown(problem.markdown_style, 'tex' if math_engine == 'jax' else math_engine)|reference|absolutify(url)|str|safe }} + {{ description|markdown(problem.markdown_style, 'tex')|reference|absolutify(url)|str|safe }}
-{% if math_engine == 'jax' %} - - - -{% endif %} + + +