Source code for shuup.admin.utils.urls

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

import inspect
import json
import six
import warnings
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import ImproperlyConfigured
from django.http.response import HttpResponseForbidden
from django.utils.encoding import force_str, force_text
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _

from shuup.admin.module_registry import get_modules
from shuup.admin.shop_provider import get_shop
from shuup.admin.utils.permissions import get_missing_permissions
from shuup.utils import importing
from shuup.utils.django_compat import NoReverseMatch, URLPattern, get_callable, is_authenticated, reverse
from shuup.utils.excs import Problem

try:
    from urllib.parse import parse_qsl
except ImportError:  # pragma: no cover
    from urlparse import parse_qsl  # Python 2.7


[docs]class AdminRegexURLPattern(URLPattern): def __init__(self, regex, callback, default_args=None, name=None, require_authentication=True, permissions=()): self.permissions = tuple(permissions) self.require_authentication = require_authentication if callable(callback): callback = self.wrap_with_permissions(callback) from django.urls import re_path repath = re_path(regex, callback, default_args, name) pattern = repath.pattern super(AdminRegexURLPattern, self).__init__(pattern, callback, default_args, name) def _get_unauth_response(self, request, reason): """ Get an error response (or raise a Problem) for a given request and reason message. :type request: Request. :param request: HttpRequest :type reason: Reason string. :param reason: str """ if request.is_ajax(): return HttpResponseForbidden(json.dumps({"error": force_text(reason)})) error_params = urlencode({"error": force_text(reason)}) login_url = force_str(reverse("shuup_admin:login") + "?" + error_params) resp = redirect_to_login(next=request.path, login_url=login_url) if is_authenticated(request.user): # Instead of redirecting to the login page, let the user know what's wrong with # a helpful link. raise ( Problem(_("Can't view this page. %(reason)s") % {"reason": reason}).with_link( url=resp.url, title=_("Log in with different credentials...") ) ) return resp def _get_unauth_reason(self, request): """ Figure out if there's any reason not to allow the user access to this view via the given request. :type request: Request. :param request: HttpRequest :rtype: str|None """ if self.require_authentication: if not is_authenticated(request.user): return _("Sign in to continue.") elif not getattr(request.user, "is_staff", False): return _("Your account must have `Access to Admin Panel` permissions to access this page.") elif not get_shop(request): return _("There is no active shop available. Contact support for more details.") missing_permissions = get_missing_permissions(request.user, self.permissions) if missing_permissions: return _("You do not have the required permissions: %s") % ", ".join(missing_permissions)
[docs] def wrap_with_permissions(self, view_func): if callable(getattr(view_func, "as_view", None)): view_func = view_func.as_view() @six.wraps(view_func) def _wrapped_view(request, *args, **kwargs): unauth_reason = self._get_unauth_reason(request) if unauth_reason: return self._get_unauth_response(request, unauth_reason) return view_func(request, *args, **kwargs) return _wrapped_view
@property def callback(self): if self._callback is not None: return self._callback callback = get_callable(self._callback_str) self._callback = self.wrap_with_permissions(callback) return self._callback @callback.setter def callback(self, value): self._callback = value
[docs]def admin_url(regex, view, kwargs=None, name=None, prefix="", require_authentication=True, permissions=None): if permissions is None: permissions = (name,) if name else () if isinstance(view, six.string_types): if not view: raise ImproperlyConfigured("Error! Empty URL pattern view name not permitted (for pattern `%r`)." % regex) if prefix: view = prefix + "." + view view = importing.load(view) return AdminRegexURLPattern( regex, view, kwargs, name, require_authentication=require_authentication, permissions=permissions )
[docs]def get_edit_and_list_urls(url_prefix, view_template, name_template, permissions=()): """ Get a list of edit/new/list URLs for (presumably) an object type with standardized URLs and names. :param url_prefix: What to prefix the generated URLs with. E.g. `"^taxes/tax"`. :type url_prefix: str :param view_template: A template string for the dotted name of the view class. E.g. "shuup.admin.modules.taxes.views.Tax%sView". :type view_template: str :param name_template: A template string for the URLnames. E.g. "tax.%s". :type name_template: str :return: List of URLs. :rtype: list[AdminRegexURLPattern] """ if permissions: warnings.warn( "Warning! `get_edit_and_list_urls` permissions attribute will be " "deprecated in Shuup 2.0 as unused for this util.", DeprecationWarning, ) return [ admin_url( r"%s/(?P<pk>\d+)/$" % url_prefix, view_template % "Edit", name=name_template % "edit", permissions=(name_template % "edit",), ), admin_url( "%s/new/$" % url_prefix, view_template % "Edit", name=name_template % "new", kwargs={"pk": None}, permissions=(name_template % "new",), ), admin_url( "%s/$" % url_prefix, view_template % "List", name=name_template % "list", permissions=(name_template % "list",), ), admin_url( "%s/list-settings/" % url_prefix, "shuup.admin.modules.settings.views.ListSettingsView", name=name_template % "list_settings", permissions=(name_template % "list_settings",), ), ]
[docs]class NoModelUrl(ValueError): pass
[docs]def get_model_url( object, kind="detail", user=None, required_permissions=None, shop=None, raise_permission_denied=False, **kwargs ): """ Get a an admin object URL for the given object or object class by interrogating each admin module. If a user is provided, checks whether user has correct permissions before returning URL. Raises `NoModelUrl` if lookup fails :param object: Model or object class. :type object: class :param kind: URL kind. Currently "new", "list", "edit", "detail". :type kind: str :param user: Optional instance to check for permissions. :type user: django.contrib.auth.models.User|None :param required_permissions: Optional iterable of permission strings. :type required_permissions: Iterable[str]|None :param shop: The shop that owns the resource. :type request: shuup.core.models.Shop|None :param raise_permission_denied: raise PermissionDenied exception if the url is found but user has not permission. If false, None will be returned instead. Default is False. :type raise_permission_denied: bool :return: Resolved URL. :rtype: str """ for module in get_modules(): url = module.get_model_url(object, kind, shop) if not url: continue if user is None: return url from shuup.utils.django_compat import Resolver404, resolve try: if required_permissions is not None: warnings.warn( "Warning! `required_permissions` parameter will be deprecated " "in Shuup 2.0 as unused for this util.", DeprecationWarning, ) permissions = required_permissions else: resolved = resolve(url) from shuup.admin.utils.permissions import get_permissions_for_module_url permissions = get_permissions_for_module_url(module, resolved.url_name) missing_permissions = get_missing_permissions(user, permissions) if not missing_permissions: return url if raise_permission_denied: from django.core.exceptions import PermissionDenied reason = _("Can't view this page. You do not have the required permission(s): `{permissions}`.").format( permissions=", ".join(missing_permissions) ) raise PermissionDenied(reason) except Resolver404: # what are you doing developer? return url raise NoModelUrl("Error! Can't get object URL of kind %s: %r." % (kind, force_text(object)))
[docs]def derive_model_url(model_class, urlname_prefix, object, kind): """ Try to guess a model URL for the given `object` and `kind`. An utility for people implementing `get_model_url`. :param model_class: The model class the object must be an instance or subclass of. :type model_class: class :param urlname_prefix: URLname prefix. For instance, `shuup_admin:shop_product.` :type urlname_prefix: str :param object: The model or model class as passed to `get_model_url`. :type object: django.db.models.Model|class :param kind: URL kind as passed to `get_model_url`. :type kind: str :return: Resolved URL or None. :rtype: str|None """ if not (isinstance(object, model_class) or (inspect.isclass(object) and issubclass(object, model_class))): return kind_to_urlnames = { "detail": ("%s.detail" % urlname_prefix, "%s.edit" % urlname_prefix), } kwarg_sets = [{}] if getattr(object, "pk", None): kwarg_sets.append({"pk": object.pk}) for urlname in kind_to_urlnames.get(kind, ["%s.%s" % (urlname_prefix, kind)]): for kwargs in kwarg_sets: try: return reverse(urlname, kwargs=kwargs) except NoReverseMatch: pass # No match whatsoever. return None
[docs]def manipulate_query_string(url, **qs): if "?" in url: url, current_qs = url.split("?", 1) qs = dict(parse_qsl(current_qs), **qs) qs = [(key, value) for (key, value) in qs.items() if value is not None] if qs: return "%s?%s" % (url, urlencode(qs)) else: return url
[docs]def get_model_front_url(request, object): """ Get a frontend URL for an object. :param request: Request. :type request: HttpRequest :param object: A model instance. :type object: django.db.models.Model :return: URL or None. :rtype: str|None """ # TODO: This method could use an extension point for alternative frontends. if not object.pk: return None if "shuup.front" in settings.INSTALLED_APPS: # Best effort to use the default frontend for front URLs. try: from shuup.front.template_helpers.urls import model_url return model_url({"request": request}, object) except (ValueError, NoReverseMatch): pass return None
[docs]def get_front_url(context): """ Get front URL for admin navigation. 1. Use front URL from view context if passed. 2. Fallback to index. """ front_url = context.get("front_url") if not front_url: try: front_url = reverse("shuup:index") except NoReverseMatch: front_url = None return front_url