Skip to content
utils.py 44.1 KiB
Newer Older
                  f"""
                  <script type="text/javascript">
                    rlCodemirror.editorFromTextArea(
                        document.getElementById('id_{name}'),
                        {repr_js(extensions)},
                        {repr_js(self.autofocus)},
                        {repr_js(additional_keys)}
                        )
                  </script>
                  """]

        return mark_safe("\n".join(output))
        interaction_mode: str | None,
        additional_keys: dict[str, JsLiteral] | None = None,
        ) -> tuple[CodeMirrorTextarea, str]:
    if additional_keys is None:
        additional_keys = {}
    from django.urls import reverse
    help_text = (_("Press Esc then Tab to leave the editor. ")
            + _("Set editor mode in <a href='%s'>user profile</a>.")
            % reverse("relate-user_profile"))
    if language_mode in ["python", "yaml"]:
        indent_unit = 4
    else:
        indent_unit = 2

    return CodeMirrorTextarea(
                    language_mode=language_mode,
                    interaction_mode=interaction_mode,
                    indent_unit=indent_unit,
                    autofocus=autofocus,
                    additional_keys=additional_keys,
                    ), help_text
# {{{ prosemirror

class ProseMirrorTextarea(forms.Textarea):
    @property
    def media(self):
        return forms.Media(js=["bundle-prosemirror.js"])

    def render(self, name, value, attrs=None, renderer=None) -> SafeString:
        output = [super().render(
                        name, value, attrs, renderer),
                  f"""
                  <script type="text/javascript">
                    rlProsemirror.editorFromTextArea(
                        document.getElementById('id_{name}'),
                        )
                  </script>
                  """]

        return mark_safe("\n".join(output))

    math_help_text = mark_safe(r"""
    See the <a href="https://katex.org/docs/supported.html"
            >list of supported math commands</a>.
    More tips for using this editor to type math:
    <ul>
        <li>
        You may paste in Markdown-with-math (as accepted by
        <a
        href="https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions"
        >Github</a>,
        <a href="https://pandoc.org/MANUAL.html#math">Pandoc</a>, or
        <a href="https://meta.discourse.org/t/discourse-math/65770">Discourse</a>).
        <li>
        Inline math nodes are delimited with <code>$</code>.
        After typing the closing dollar sign in
        an expression like <code>$\int_a^b f(x) dx$</code>, a math node will appear.
        </li>

        <li>
        To start a block math node, press Enter to create a blank line,
        then type <code>$$</code> followed by Space. You can type multi-line math
        expressions, and the result will render in display style.
        </li>
        <li>
        Math nodes behave like regular text when using arrow keys or Backspace.
        From within a math node, press Ctrl-Backspace to delete the entire node.
        You can select, copy, and paste math nodes just like regular text!
        </li>
    </ul>
    """)

# }}}


def get_facilities_config(
        request: http.HttpRequest | None = None
        ) -> dict[str, dict[str, Any]] | None:
    from django.conf import settings

    # This is called during offline validation, where Django isn't really set up.
    # The getattr makes this usable.
    facilities = getattr(settings, "RELATE_FACILITIES", None)
    if facilities is None:
        # Only happens during offline validation. Suppresses errors there.
        return None

    if callable(facilities):
        from course.views import get_now_or_fake_time
        now_datetime = get_now_or_fake_time(request)

        result = facilities(now_datetime)
        if not isinstance(result, dict):
            raise RuntimeError("RELATE_FACILITIES must return a dictionary")
        return result
    else:
        return facilities


    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request: http.HttpRequest) -> http.HttpResponse:
        pretend_facilities = request.session.get("relate_pretend_facilities")

        if pretend_facilities is not None:
            facilities = pretend_facilities
        else:
            remote_address = remote_address_from_request(request)
            facilities_config = get_facilities_config(request)
            if facilities_config is None:
                facilities_config = {}

            from ipaddress import ip_network
            for name, props in facilities_config.items():
                ip_ranges = props.get("ip_ranges", [])
                for ir in ip_ranges:
                    if remote_address in ip_network(str(ir)):
        request = cast(RelateHttpRequest, request)
        request.relate_facilities = frozenset(facilities)
        return self.get_response(request)

def get_col_contents_or_empty(row, index):
    if index >= len(row):
        return ""
    else:
        return row[index]


def csv_data_importable(file_contents, column_idx_list, header_count):
    import csv
    spamreader = csv.reader(file_contents)
    n_header_row = 0
Dong Zhuang's avatar
Dong Zhuang committed
    try:
        row0 = spamreader.__next__()
Dong Zhuang's avatar
Dong Zhuang committed
    except Exception as e:
        err_msg = type(e).__name__
        err_str = str(e)
        if err_msg == "Error":
            err_msg = ""
        else:
            err_msg += ": "
        err_msg += err_str

        if "line contains NUL" in err_str:
Dong Zhuang's avatar
Dong Zhuang committed
            err_msg = err_msg.rstrip(".") + ". "

            # This message changed over time.
            # Make the message uniform to please the tests.
            err_msg = err_msg.replace("NULL byte", "NUL")

Dong Zhuang's avatar
Dong Zhuang committed
            err_msg += _("Are you sure the file is a CSV file other "
                         "than a Microsoft Excel file?")

        return False, (
            string_concat(
                pgettext_lazy("Starting of Error message", "Error"),
                f": {err_msg}"))
Dong Zhuang's avatar
Dong Zhuang committed

    from itertools import chain

    for row in chain([row0], spamreader):
        n_header_row += 1
        if n_header_row <= header_count:
            continue
        try:
            for column_idx in column_idx_list:
                if column_idx is not None:
                    str(get_col_contents_or_empty(row, column_idx-1))
        except UnicodeDecodeError:
            return False, (
                    _("Error: Columns to be imported contain "
                        "non-ASCII characters. "
                        "Please save your CSV file as utf-8 encoded "
                        "and import again.")
            )
        except Exception as e:
            return False, (
                    string_concat(
                        pgettext_lazy("Starting of Error message",
                            "Error"),
                        ": %(err_type)s: %(err_str)s")
                    % {
                        "err_type": type(e).__name__,
                        "err_str": str(e)}
                    )
def will_use_masked_profile_for_email(recipient_email: str | list[str] | None) -> bool:
    if not recipient_email:
        return False
    if not isinstance(recipient_email, list):
        recipient_email = [recipient_email]
    from course.models import Participation
    recipient_participations = (
        Participation.objects.filter(
            user__email__in=recipient_email
        ))
    from course.constants import participation_permission as pperm
    for part in recipient_participations:
        if part.has_permission(pperm.view_participant_masked_profile):
            return True
    return False

def get_course_specific_language_choices() -> tuple[tuple[str, Any], ...]:

    from collections import OrderedDict

Andreas Klöckner's avatar
Andreas Klöckner committed
    from django.conf import settings

Andreas Klöckner's avatar
Andreas Klöckner committed
    all_options = ((settings.LANGUAGE_CODE, None), *tuple(settings.LANGUAGES))
    filtered_options_dict = OrderedDict(all_options)

    def get_default_option() -> tuple[str, str]:
        # For the default language used, if USE_I18N is True, display
        # "Disabled". Otherwise display its lang info.
        if not settings.USE_I18N:
            formatted_descr = (
                get_formatted_options(settings.LANGUAGE_CODE, None)[1])
        else:
            formatted_descr = _("disabled (i.e., displayed language is "
                                "determined by user's browser preference)")
        return "", string_concat("{}: ".format(_("Default")), formatted_descr)
    def get_formatted_options(
            lang_code: str, lang_descr: str | None) -> tuple[str, str]:
        if lang_descr is None:
            lang_descr = OrderedDict(settings.LANGUAGES).get(lang_code)
            if lang_descr is None:
                try:
                    lang_info = translation.get_language_info(lang_code)
                    lang_descr = lang_info["name_translated"]
                except KeyError:
                    return (lang_code.strip(), lang_code)

        return (lang_code.strip(),
                string_concat(_(lang_descr), f" ({lang_code})"))

    filtered_options = (
        [get_default_option()]
        + [get_formatted_options(k, v)
           for k, v in filtered_options_dict.items()])

    # filtered_options[1] is the option for settings.LANGUAGE_CODE
    # it's already displayed when settings.USE_I18N is False
    if not settings.USE_I18N:
        filtered_options.pop(1)

    return tuple(filtered_options)

class LanguageOverride(ContextDecorator):
    def __init__(self, course: Course, deactivate: bool = False) -> None:
        self.course = course
        self.deactivate = deactivate

        if course.force_lang:
            self.language = course.force_lang
        else:
            from django.conf import settings
            self.language = settings.RELATE_ADMIN_EMAIL_LOCALE

        self.old_language = translation.get_language()
        if self.language is not None:
            translation.activate(self.language)
        else:
            translation.deactivate_all()

    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
        if self.old_language is None:
            translation.deactivate_all()
        elif self.deactivate:
            translation.deactivate()
        else:
            translation.activate(self.old_language)


class RelateJinjaMacroBase:
    def __init__(
            self,
            course: Course | None,
            repo: Repo_ish,
            commit_sha: bytes) -> None:
        self.course = course
        self.repo = repo
        self.commit_sha = commit_sha

    @property
    def name(self):
        # The name of the method used in the template
        raise NotImplementedError()

    def __call__(self, *args: Any, **kwargs: Any) -> str:
        raise NotImplementedError()


Andreas Klöckner's avatar
Andreas Klöckner committed
# vim: foldmethod=marker