Skip to content

Commit

Permalink
feat: apply requested changes
Browse files Browse the repository at this point in the history
  • Loading branch information
janbritz committed Aug 20, 2024
1 parent d0c91d1 commit f4d1416
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 90 deletions.
26 changes: 13 additions & 13 deletions questionpy_sdk/webserver/attempt.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def get_attempt_render_context(
) -> _AttemptRenderContext:
renderer_args = (attempt.ui.placeholders, display_options, seed, last_attempt_data)

formulation_renderer = QuestionFormulationUIRenderer(attempt.ui.formulation, *renderer_args)
html, errors = QuestionFormulationUIRenderer(attempt.ui.formulation, *renderer_args).render()

context: _AttemptRenderContext = {
"attempt_status": (
Expand All @@ -54,28 +54,28 @@ def get_attempt_render_context(
"attempt_state": attempt_state,
"options": display_options.model_dump(include={"general_feedback", "feedback", "right_answer"}),
"form_disabled": disabled,
"formulation": formulation_renderer.html,
"formulation": html,
"attempt": attempt,
"general_feedback": None,
"specific_feedback": None,
"right_answer": None,
"render_errors": {},
}

if formulation_renderer.errors:
context["render_errors"]["Formulation"] = formulation_renderer.errors
if errors:
context["render_errors"]["Formulation"] = errors
if display_options.general_feedback and attempt.ui.general_feedback:
renderer = QuestionUIRenderer(attempt.ui.general_feedback, *renderer_args)
context["general_feedback"] = renderer.html
context["render_errors"]["General Feedback"] = renderer.errors
html, errors = QuestionUIRenderer(attempt.ui.general_feedback, *renderer_args).render()
context["general_feedback"] = html
context["render_errors"]["General Feedback"] = errors
if display_options.feedback and attempt.ui.specific_feedback:
renderer = QuestionUIRenderer(attempt.ui.specific_feedback, *renderer_args)
context["specific_feedback"] = renderer.html
context["render_errors"]["Specific Feedback"] = renderer.errors
html, errors = QuestionUIRenderer(attempt.ui.specific_feedback, *renderer_args).render()
context["specific_feedback"] = html
context["render_errors"]["Specific Feedback"] = errors
if display_options.right_answer and attempt.ui.right_answer:
renderer = QuestionUIRenderer(attempt.ui.right_answer, *renderer_args)
context["right_answer"] = renderer.html
context["render_errors"]["Right Answer"] = renderer.errors
html, errors = QuestionUIRenderer(attempt.ui.right_answer, *renderer_args).render()
context["right_answer"] = html
context["render_errors"]["Right Answer"] = errors

log_render_errors(context["render_errors"])

Expand Down
48 changes: 26 additions & 22 deletions questionpy_sdk/webserver/question_ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import re
from enum import StrEnum
from functools import cached_property
from random import Random
from typing import Any

Expand Down Expand Up @@ -191,14 +190,14 @@ def __init__(
attempt: dict | None = None,
) -> None:
xml = self._replace_qpy_urls(xml)
self.errors = RenderErrorCollection()
self._errors = RenderErrorCollection()

try:
root = etree.fromstring(xml)
except etree.XMLSyntaxError as error:
parser = etree.XMLParser(recover=True)
root = etree.fromstring(xml, parser=parser)
self.errors.insert(XMLSyntaxError(error=error))
self._errors.insert(XMLSyntaxError(error=error))

self._xml = etree.ElementTree(root)
self._xpath = etree.XPathDocumentEvaluator(self._xml)
Expand All @@ -208,25 +207,30 @@ def __init__(
self._options = options
self._random = Random(seed)
self._attempt = attempt
self._html = None

@cached_property
def html(self) -> str:
self._render()
return etree.tostring(self._xml, pretty_print=True, method="html").decode()

def _render(self) -> None:
"""Applies transformations to the xml."""
self._resolve_placeholders()
self._hide_unwanted_feedback()
self._hide_if_role()
self._set_input_values_and_readonly()
self._soften_validation()
self._defuse_buttons()
self._shuffle_contents()
self._add_styles()
self._format_floats()
# TODO: mangle_ids_and_names
self._clean_up()
def render(self) -> tuple[str, RenderErrorCollection]:
"""Applies transformations to the xml.
Returns:
tuple: The rendered html and a render errors collection.
"""
if self._html is None:
self._resolve_placeholders()
self._hide_unwanted_feedback()
self._hide_if_role()
self._set_input_values_and_readonly()
self._soften_validation()
self._defuse_buttons()
self._shuffle_contents()
self._add_styles()
self._format_floats()
# TODO: mangle_ids_and_names
self._clean_up()

self._html = etree.tostring(self._xml, pretty_print=True, method="html").decode()

return self._html, self._errors

def _replace_qpy_urls(self, xml: str) -> str:
"""Replace QPY-URLs to package files with SDK-URLs."""
Expand Down Expand Up @@ -293,7 +297,7 @@ def _hide_unwanted_feedback(self) -> None:
error = InvalidAttributeValueError(
element=element, attribute="qpy:feedback", value=feedback_type or "", expected=expected
)
self.errors.insert(error)
self._errors.insert(error)

def _hide_if_role(self) -> None:
"""Hides elements based on user role.
Expand Down
25 changes: 12 additions & 13 deletions questionpy_sdk/webserver/question_ui/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from bisect import insort
from collections.abc import Iterable, Iterator, Sized
from dataclasses import dataclass
from functools import cached_property
from operator import attrgetter
from typing import TypeAlias

Expand All @@ -20,22 +19,22 @@
class RenderError(ABC):
"""Represents a generic error which occurred during rendering."""

@cached_property
@property
@abstractmethod
def line(self) -> int | None:
"""Original line number where the error occurred or None if unknown."""

@cached_property
@property
def order(self) -> int:
"""Can be used to order multiple errors."""
return self.line or 0

@cached_property
@property
@abstractmethod
def message(self) -> str:
pass

@cached_property
@property
def html_message(self) -> str:
return html.escape(self.message)

Expand All @@ -44,13 +43,13 @@ def html_message(self) -> str:
class RenderElementError(RenderError, ABC):
element: etree._Element

@cached_property
@property
def element_representation(self) -> str:
# Create the prefix of an element. We do not want to keep 'html' as a prefix.
prefix = f"{self.element.prefix}:" if self.element.prefix and self.element.prefix != "html" else ""
return prefix + etree.QName(self.element).localname

@cached_property
@property
def line(self) -> int | None:
"""Original line number as found by the parser or None if unknown."""
return self.element.sourceline # type: ignore[return-value]
Expand Down Expand Up @@ -81,11 +80,11 @@ def _message(self, *, as_html: bool) -> str:
f"on element {opening}{self.element_representation}{closing}.{expected}"
)

@cached_property
@property
def message(self) -> str:
return self._message(as_html=False)

@cached_property
@property
def html_message(self) -> str:
return self._message(as_html=True)

Expand All @@ -96,20 +95,20 @@ class XMLSyntaxError(RenderError):

error: etree.XMLSyntaxError

@cached_property
@property
def line(self) -> int | None:
return self.error.lineno

@cached_property
@property
def order(self) -> int:
# Syntax errors can lead to a multitude of other errors therefore we want them to be the first in order.
return -1

@cached_property
@property
def message(self) -> str:
return f"Syntax error: {self.error.msg}"

@cached_property
@property
def html_message(self) -> str:
return f"Invalid syntax: <samp>{html.escape(self.error.msg)}</samp>"

Expand Down
Loading

0 comments on commit f4d1416

Please sign in to comment.