src/util/url_utils.py
from urllib.parse import parse_qsl, urlparse, urlunparse from ckeditor_uploader import views as ckeditor_viewsfrom django.conf import settingsfrom django.conf.urls.i18n import i18n_patternsfrom django.contrib.auth import views as auth_viewsfrom django.contrib.auth.decorators import user_passes_testfrom django.core.exceptions import PermissionDeniedfrom django.urls import include, pathfrom django.utils import regex_helperfrom django.utils.http import urlencodefrom django.views.decorators.cache import never_cachefrom django_hosts import reversefrom django_hosts.defaults import hostfrom django_hosts.middleware import HostsBaseMiddleware def urljoin_query(base_url: str, query: dict | str): """ A version of `urljoin() <https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urljoin>`_ that adds the query parameters in ``query`` to ``base_url``. """ if isinstance(query, str): query = dict(parse_qsl(query.strip().lstrip("?"))) # Based on https://stackoverflow.com/a/2506477 url_parts = list(urlparse(base_url)) base_query = dict(parse_qsl(url_parts[4])) url_parts[4] = urlencode(base_query | query) return urlunparse(url_parts) def reverse_internal(viewname: str, *args): return reverse(viewname, args=args, host='internal', host_args=['i']) def reverse_admin(viewname: str, args=None, **kwargs): return reverse(f'admin:{viewname}', args=args, kwargs=kwargs, host='admin') def reverse_docs(viewname: str, *args): return reverse(viewname, args=args, host='docs') def get_host_object_from_url(url: str) -> host: """ This does kind of the opposite of ``django_hosts.resolvers.reverse_host()`` (which returns the host part of a URL from :class:`django_hosts.defaults.host` arguments), in that a :class:`django_hosts.defaults.host` instance is returned from the host part of the passed ``url``. :raises ValueError: if the passed ``url`` is not an internal URL, i.e. if the URL's host doesn't contain``settings.PARENT_HOST`` """ url_host = urlparse(url).netloc if settings.PARENT_HOST not in url_host: raise ValueError(f"The passed URL ({url}) must be internal, i.e. {settings.PARENT_HOST} must be part of the URL's host.") hosts_middleware = HostsBaseMiddleware(get_response=lambda request: None) host_obj, _kwargs = hosts_middleware.get_host(url_host) return host_obj def get_reverse_host_kwargs_from_url(url: str) -> dict: """ :return: A ``dict`` containing the ``host`` and ``host_args`` kwargs that can be passed to ``django_hosts.resolvers.reverse_host()`` to return a URL on the same subdomain as the passed ``url``. """ host_obj = get_host_object_from_url(url) result__params__tuples = regex_helper.normalize(host_obj.regex)Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. assert len(result__params__tuples) == 1 _result, params = result__params__tuples[0] if params: url_host = urlparse(url).netloc subdomain, _rest = url_host.split(settings.PARENT_HOST, maxsplit=1) subdomain = subdomain.strip(".") else: subdomain = None return { 'host': host_obj.name, 'host_args': subdomain, } # Code based on https://github.com/django/django/blob/9c19aff7c7561e3a82978a272ecdaad40dda5c00/django/contrib/auth/decorators.py#L60-L82Function `permission_required_else_denied` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.def permission_required_else_denied(perm, login_url=None): """ Decorator for views that checks whether a user has a particular permission. When the user does not have the permission, they will be redirected to the login page if not logged in, and a ``PermissionDenied`` will be raised if already logged in. """ from users.models import User # Avoids circular importing def check_perms(user: User): if not user.is_authenticated: # Will make `user_passes_test()` redirect the user to the login page return False if isinstance(perm, str): perms = (perm,) else: perms = perm if user.has_perms(perms): return True raise PermissionDenied return user_passes_test(check_perms, login_url=login_url) def logout_urls(): """ Should be used in every URL config referenced in ``web/hosts.py``, so that the user can log out from each of the corresponding subdomains. DEV: It should technically be possible to only have one logout URL on e.g. the main subdomain, but when submitting the logout form on other subdomains (i.e. pressing the "Log out" button), multiple browsers (like Chrome) send an ``Origin`` header with a value of ``null``, which causes the CSRF verification to always fail. """ from dataporten import views as dataporten_views # Avoids circular importing # Both of these views log the user out, then redirects to the value of the `LOGOUT_REDIRECT_URL` setting if settings.USES_DATAPORTEN_AUTH: logout_view = dataporten_views.Logout.as_view() else: logout_view = auth_views.LogoutView.as_view() return i18n_patterns( # This path's `name` should be the same as the one for Django admin's logout URL # (https://github.com/django/django/blob/4.1.7/django/contrib/admin/sites.py#L270), # so that this path can override it, and consequently be used as intended in `admin_urls.py` path("logout/", logout_view, name='logout'), prefix_default_language=False, ) def ckeditor_uploader_urls(): """ Should be used in every URL config referenced in ``web/hosts.py``, so that the CKEditor uploader widgets work correctly. NOTE: The paths returned by this function must be used as top-level paths, with no prefix between the URL host and these paths (i.e. for example ``makentnu.no/<these paths>``). This is so that the CKEditor uploader widgets can reverse these paths regardless of the subdomain. """ return [ # Based on https://github.com/django-ckeditor/django-ckeditor/blob/9866ebe098794eca7a5132d6f2a4b1d1d837e735/ckeditor_uploader/urls.py#L8-L13 path("ckeditor/upload/", permission_required_else_denied('contentbox.can_upload_image')(ckeditor_views.upload), name='ckeditor_upload'), path("ckeditor/browse/", never_cache(permission_required_else_denied('contentbox.can_browse_image')(ckeditor_views.browse)), name='ckeditor_browse'), ] def debug_toolbar_urls(): if not settings.USE_DEBUG_TOOLBAR: return [] return [ path("__debug__/", include('debug_toolbar.urls')), ]