From 5365009f370d1218e5fe618851621abd9829153d Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 7 Nov 2024 21:06:09 +0100 Subject: [PATCH] feat: LinkDict (syntactic sugar) --- README.rst | 20 +++-- djangocms_link/__init__.py | 2 +- djangocms_link/admin.py | 63 +++++++++---- djangocms_link/apps.py | 10 ++- djangocms_link/cms_plugins.py | 42 +++++---- djangocms_link/fields.py | 88 +++++++++++++------ djangocms_link/helpers.py | 54 +++++++++++- djangocms_link/models.py | 45 +++++----- .../templatetags/djangocms_link_tags.py | 5 +- djangocms_link/validators.py | 63 ++++++++----- tests/settings.py | 51 ++++++----- tests/test_endpoint.py | 41 ++++++--- tests/test_fields.py | 40 ++++++--- tests/test_link_dict.py | 57 ++++++++++++ tests/test_migrations.py | 58 ++++++------ tests/test_models.py | 32 ++++--- tests/test_plugins.py | 56 ++++++------ tests/test_validators.py | 11 ++- 18 files changed, 499 insertions(+), 239 deletions(-) create mode 100644 tests/test_link_dict.py diff --git a/README.rst b/README.rst index f84e16c0..44ea6f21 100644 --- a/README.rst +++ b/README.rst @@ -195,10 +195,12 @@ models or forms. class MyForm(forms.Form): link = LinkFormField(required=False) -``LinkField`` is a subclass of ``JSONField`` and stores the link data as dict. +``LinkField`` is a subclass of ``JSONField`` and stores the link data as +``djangocms_link.helpers.LinkDict``, a direct subclass of ``dict``. (An empty link will be ``{}``.) -To render the link field in a template, use the new template tag ``to_url``:: +To render the link field in a template, use the ``LinkDict`` property ``url`` or +the new template tag ``to_url``:: {% load djangocms_link_tags %} {# Variant 1 #} @@ -207,18 +209,22 @@ To render the link field in a template, use the new template tag ``to_url``:: {% endif %} {# Variant 2 #} + {% if obj.link %} + Link available + {% endif %} + + {# Variant 3 #} {% with url=obj.link|to_url %} {% if url %} Link available {% endif %} -To turn the ``LinkField``'s dictionary into a URL in python code, use the -``djangocms_link.helpers.get_link`` helper function:: - - from djangocms_link.helpers import get_link +To turn the ``LinkField``'s ``LinkDict`` dictionary into a URL in python code, +use the ``url`` property. (It will hit the database when needed. Results are +cached.):: obj = MyModel.objects.first() - url = get_link(obj.link) + url = obj.link.url Running Tests diff --git a/djangocms_link/__init__.py b/djangocms_link/__init__.py index 344ac95d..10daf977 100644 --- a/djangocms_link/__init__.py +++ b/djangocms_link/__init__.py @@ -1 +1 @@ -__version__ = '5.0.0a1' +__version__ = "5.0.0a1" diff --git a/djangocms_link/admin.py b/djangocms_link/admin.py index 559ad4b9..43a25b01 100644 --- a/djangocms_link/admin.py +++ b/djangocms_link/admin.py @@ -12,6 +12,7 @@ from cms.utils import get_language_from_request from . import models +from .fields import LinkFormField, LinkWidget from .helpers import get_manager @@ -25,6 +26,7 @@ class GrouperModelAdmin: pass + REGISTERED_ADMIN = [] # Will be set by djangocms_link.apps.DjangoCmsLinkConfig.ready @@ -117,20 +119,42 @@ def get_queryset(self): """Return queryset based on ModelAdmin.get_search_results().""" try: # django CMS 4.2+ - qs = PageContent.admin_manager.filter(language=self.language).filter( - Q(title__icontains=self.term) | Q(menu_title__icontains=self.term) - ).current_content() - qs = (Page.objects.filter(pk__in=qs.values_list("page_id", flat=True)).order_by("path") - .annotate(__link_text__=Subquery(qs.filter(page_id=OuterRef("pk")).values("title")[:1]))) + qs = ( + PageContent.admin_manager.filter(language=self.language) + .filter( + Q(title__icontains=self.term) | Q(menu_title__icontains=self.term) + ) + .current_content() + ) + qs = ( + Page.objects.filter(pk__in=qs.values_list("page_id", flat=True)) + .order_by("path") + .annotate( + __link_text__=Subquery( + qs.filter(page_id=OuterRef("pk")).values("title")[:1] + ) + ) + ) if self.site: qs = qs.filter(site_id=self.site) except (AttributeError, FieldError): # django CMS 3.11 - 4.1 - qs = get_manager(PageContent, current_content=True).filter(language=self.language).filter( - Q(title__icontains=self.term) | Q(menu_title__icontains=self.term) + qs = ( + get_manager(PageContent, current_content=True) + .filter(language=self.language) + .filter( + Q(title__icontains=self.term) | Q(menu_title__icontains=self.term) + ) + ) + qs = ( + Page.objects.filter(pk__in=qs.values_list("page_id", flat=True)) + .order_by("node__path") + .annotate( + __link_text__=Subquery( + qs.filter(page_id=OuterRef("pk")).values("title")[:1] + ) + ) ) - qs = (Page.objects.filter(pk__in=qs.values_list("page_id", flat=True)).order_by("node__path") - .annotate(__link_text__=Subquery(qs.filter(page_id=OuterRef("pk")).values("title")[:1]))) if self.site: qs = qs.filter(node__site_id=self.site) return list(qs) @@ -148,7 +172,9 @@ def add_admin_querysets(self, qs): else: new_qs = model_admin.get_queryset(self.request) if hasattr(model_admin.model, "site") and self.site: - new_qs = new_qs.filter(Q(site_id=self.site) | Q(site__isnull=True)) + new_qs = new_qs.filter( + Q(site_id=self.site) | Q(site__isnull=True) + ) elif hasattr(model_admin.model, "sites") and self.site: new_qs = new_qs.filter(sites__id=self.site) new_qs, search_use_distinct = model_admin.get_search_results( @@ -169,7 +195,9 @@ def process_request(self, request): Validate request integrity, extract and return request parameters. """ term = request.GET.get("term", request.GET.get("q", "")).strip("  ").lower() - site = request.GET.get("app_label", "") # Django admin's app_label is abused as site id + site = request.GET.get( + "app_label", "" + ) # Django admin's app_label is abused as site id try: site = int(site) except ValueError: @@ -191,6 +219,10 @@ class LinkAdmin(admin.ModelAdmin): """The LinkAdmin class provides the endpoint for getting the urls. It is not visible in the admin interface.""" + global_link_form_widget = LinkWidget + global_link_form_field = LinkFormField + global_link_url_name = None + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.global_link_url_name = f"{self.opts.app_label}_{self.opts.model_name}_urls" @@ -202,10 +234,11 @@ def has_module_permission(self, request): # pragma: no cover def get_urls(self): # Only url endpoint public, do not call super().get_urls() return [ - path("urls", - self.admin_site.admin_view(self.url_view), - name=self.global_link_url_name - ), + path( + "urls", + self.admin_site.admin_view(self.url_view), + name=self.global_link_url_name, + ), ] def url_view(self, request): diff --git a/djangocms_link/apps.py b/djangocms_link/apps.py index d20ecfc0..475df7bd 100644 --- a/djangocms_link/apps.py +++ b/djangocms_link/apps.py @@ -26,7 +26,9 @@ def ready(self): continue # search_fields need to be defined in the ModelAdmin class, and the model needs to have # a get_absolute_url method. - if getattr(_admin, "search_fields", []) and hasattr(_admin.model, "get_absolute_url"): + if getattr(_admin, "search_fields", []) and hasattr( + _admin.model, "get_absolute_url" + ): link_admin.REGISTERED_ADMIN.append(_admin) else: # turn model config into model admin instances @@ -35,12 +37,14 @@ def ready(self): if isinstance(model, str): model = apps.get_model(model) if not hasattr(model, "get_absolute_url"): # pragma: no cover - raise ImproperlyConfigured(f"{model.__name__} needs to implement get_absolute_url method") + raise ImproperlyConfigured( + f"{model.__name__} needs to implement get_absolute_url method" + ) admin = admin.site._registry[model] if admin not in admins: admins.append(admin) elif not isinstance(model, ModelAdmin): # pragma: no cover raise ImproperlyConfigured( - "DJANGOCMS_LINK_LINKABLE_MODELS must be a list of string \"app_label.model_name\"" + 'DJANGOCMS_LINK_LINKABLE_MODELS must be a list of string "app_label.model_name"' ) link_admin.REGISTERED_ADMIN = admins diff --git a/djangocms_link/cms_plugins.py b/djangocms_link/cms_plugins.py index 5c929007..cd42e21d 100644 --- a/djangocms_link/cms_plugins.py +++ b/djangocms_link/cms_plugins.py @@ -10,7 +10,7 @@ class LinkPlugin(CMSPluginBase): model = Link - name = _('Link') + name = _("Link") text_enabled = True text_icon = ( ' dict: } if File and "file_link" in allowed_link_types: del context["widget"]["subwidgets"]["file_link"] - index = next(i for i, widget in enumerate(self.widgets) if widget.attrs.get("widget") == "file_link") - context["filer_widget"] = self.widgets[index].render(name + f"_{index}", value[index], attrs) + index = next( + i + for i, widget in enumerate(self.widgets) + if widget.attrs.get("widget") == "file_link" + ) + context["filer_widget"] = self.widgets[index].render( + name + f"_{index}", value[index], attrs + ) return context class LinkFormField(Field): widget = LinkWidget - external_link_validators = [ExtendedURLValidator(allowed_link_types=allowed_link_types)] + external_link_validators = [ + ExtendedURLValidator(allowed_link_types=allowed_link_types) + ] internal_link_validators = [] file_link_validators = [] anchor_validators = [AnchorValidator()] @@ -303,20 +327,20 @@ def prepare_value(self, value: dict) -> list[str | None]: def to_python(self, value: list[str | None]) -> dict: """Turn MultiWidget list data into LinkField dict format""" if not value: - return {} + return LinkDict() link_type = value[0] pos = self._get_pos(link_type) if not pos: # No link type selected - return {} + return LinkDict() pos_anchor = self._get_pos("anchor") - python = {link_type: value[pos]} if value[pos] else {} - if link_type == "internal_link" and pos_anchor and value[pos_anchor]: + python = LinkDict({link_type: value[pos]} if value[pos] else {}) + if python and link_type == "internal_link" and pos_anchor and value[pos_anchor]: python["anchor"] = value[pos_anchor] return python - def run_validators(self, value: dict): + def run_validators(self, value: LinkDict): """Check for _validators property and run the validators""" for link_type in link_types: if link_type in value: @@ -342,3 +366,11 @@ def __init__(self, *args, **kwargs): def formfield(self, **kwargs): kwargs.setdefault("form_class", LinkFormField) return super().formfield(**kwargs) + + def from_db_value(self, value, expression, connection): + value = super().from_db_value(value, expression, connection) + return LinkDict(value) + + def to_python(self, value): + value = super().to_python(value) + return LinkDict(value) diff --git a/djangocms_link/helpers.py b/djangocms_link/helpers.py index 4bd41f48..e417ecb1 100644 --- a/djangocms_link/helpers.py +++ b/djangocms_link/helpers.py @@ -5,9 +5,19 @@ from django.db import models +try: + from filer.models import File +except (ModuleNotFoundError, ImportError): # pragma: no cover + File = None + + def get_manager(model: models.Model, current_content: bool = False) -> models.Manager: if hasattr(model, "admin_manager"): - return model.admin_manager.current_content() if current_content else model.admin_manager + return ( + model.admin_manager.current_content() + if current_content + else model.admin_manager + ) return model.objects @@ -40,11 +50,15 @@ def get_link(link_field_value: dict, site_id: int | None = None) -> str | None: # Access site id if possible (no db access necessary) if site_id is None: site_id = Site.objects.get_current().id - obj_site_id = getattr(obj, "site_id", getattr(getattr(obj, "node", None), "site_id", None)) + obj_site_id = getattr( + obj, "site_id", getattr(getattr(obj, "node", None), "site_id", None) + ) link_field_value["__cache__"] = obj.get_absolute_url() # Can be None if obj_site_id and obj_site_id != site_id: ref_site = Site.objects._get_site_by_id(obj_site_id).domain - link_field_value["__cache__"] = f"//{ref_site}{link_field_value['__cache__']}" + link_field_value["__cache__"] = ( + f"//{ref_site}{link_field_value['__cache__']}" + ) if link_field_value["__cache__"]: link_field_value["__cache__"] += link_field_value.get("anchor", "") elif hasattr(obj, "url"): @@ -52,3 +66,37 @@ def get_link(link_field_value: dict, site_id: int | None = None) -> str | None: else: link_field_value["__cache__"] = None return link_field_value["__cache__"] + + +class LinkDict(dict): + """dict subclass with two additional properties: url and type to easily infer the link type and + the url of the link. The url property is cached to avoid multiple db lookups.""" + + def __init__(self, initial=None, **kwargs): + super().__init__(**kwargs) + if initial: + if isinstance(initial, dict): + self.update(initial) + elif isinstance(initial, str): + self["external_link"] = initial + elif isinstance(initial, File): + self["file_link"] = initial.pk + elif isinstance(initial, models.Model): + self["internal_link"] = ( + f"{initial._meta.app_label}.{initial._meta.model_name}:{initial.pk}" + ) + if "anchor" in kwargs: + self["anchor"] = kwargs["anchor"] + + @property + def url(self): + return get_link(self) or "" + + @property + def type(self): + if "internal_link" in self: + # "internal_link" is checked prior to "anchor" + return "internal_link" + if self: + return next(iter(self)) + return "" diff --git a/djangocms_link/models.py b/djangocms_link/models.py index a3684966..61635211 100644 --- a/djangocms_link/models.py +++ b/djangocms_link/models.py @@ -2,6 +2,7 @@ Enables the user to add a "Link" plugin that displays a link using the HTML tag. """ + from django.conf import settings from django.core.exceptions import ValidationError from django.db import models @@ -21,33 +22,29 @@ # Add additional choices through the ``settings.py``. def get_templates(): choices = [ - ('default', _('Default')), + ("default", _("Default")), ] choices += getattr( settings, - 'DJANGOCMS_LINK_TEMPLATES', + "DJANGOCMS_LINK_TEMPLATES", [], ) return choices -HOSTNAME = getattr( - settings, - 'DJANGOCMS_LINK_INTRANET_HOSTNAME_PATTERN', - None -) +HOSTNAME = getattr(settings, "DJANGOCMS_LINK_INTRANET_HOSTNAME_PATTERN", None) TARGET_CHOICES = ( - ('_blank', _('Open in new window')), - ('_self', _('Open in same window')), - ('_parent', _('Delegate to parent')), - ('_top', _('Delegate to top')), + ("_blank", _("Open in new window")), + ("_self", _("Open in same window")), + ("_parent", _("Delegate to parent")), + ("_top", _("Delegate to top")), ) class AbstractLink(CMSPlugin): # used by django CMS search - search_fields = ('name', ) + search_fields = ("name",) # allows link requirement to be changed when another # CMS plugin inherits from AbstractLink @@ -58,31 +55,31 @@ class AbstractLink(CMSPlugin): ] template = models.CharField( - verbose_name=_('Template'), + verbose_name=_("Template"), choices=get_templates(), default=get_templates()[0][0], max_length=255, ) name = models.CharField( - verbose_name=_('Display name'), + verbose_name=_("Display name"), blank=True, max_length=255, ) link = LinkField( - verbose_name=_('Link'), + verbose_name=_("Link"), ) # advanced options target = models.CharField( - verbose_name=_('Target'), + verbose_name=_("Target"), choices=TARGET_CHOICES, blank=True, max_length=255, ) attributes = AttributesField( - verbose_name=_('Attributes'), + verbose_name=_("Attributes"), blank=True, - excluded_keys=['href', 'target'], + excluded_keys=["href", "target"], ) # Add an app namespace to related_name to avoid field name clashes @@ -91,7 +88,7 @@ class AbstractLink(CMSPlugin): # https://github.com/divio/django-cms/issues/5030 cmsplugin_ptr = models.OneToOneField( CMSPlugin, - related_name='%(app_label)s_%(class)s', + related_name="%(app_label)s_%(class)s", parent_link=True, on_delete=models.CASCADE, ) @@ -105,8 +102,8 @@ def __str__(self): def get_short_description(self): link = self.get_link() if self.name and link: - return f'{self.name} ({link})' - return self.name or link or gettext('') + return f"{self.name} ({link})" + return self.name or link or gettext("") def get_link(self, site_id=None): return get_link(self.link, site_id) @@ -115,13 +112,13 @@ def clean(self): super().clean() if not self.link_is_optional and not self.link: raise ValidationError( - force_str(_('Link is required.')), - code='required', + force_str(_("Link is required.")), + code="required", ) def __init__(self, *args, **wkargs): super().__init__(*args, **wkargs) - self._meta.get_field('link').blank = self.link_is_optional + self._meta.get_field("link").blank = self.link_is_optional class Link(AbstractLink): diff --git a/djangocms_link/templatetags/djangocms_link_tags.py b/djangocms_link/templatetags/djangocms_link_tags.py index 3100c297..31ea9493 100644 --- a/djangocms_link/templatetags/djangocms_link_tags.py +++ b/djangocms_link/templatetags/djangocms_link_tags.py @@ -7,6 +7,7 @@ try: from filer.models import File except (ImportError, ModuleNotFoundError): # pragma: no cover + class File: pass @@ -24,5 +25,7 @@ def to_link(value): if isinstance(value, File): return {"file_link": value.pk} elif isinstance(value, models.Model): - return {"internal_link": f"{value._meta.app_label}.{value._meta.model_name}:{value.pk}"} + return { + "internal_link": f"{value._meta.app_label}.{value._meta.model_name}:{value.pk}" + } return {"external_link": value} diff --git a/djangocms_link/validators.py b/djangocms_link/validators.py index ed0caa84..4fed5ff9 100644 --- a/djangocms_link/validators.py +++ b/djangocms_link/validators.py @@ -17,32 +17,43 @@ class IntranetURLValidator(URLValidator): DJANGOCMS_LINK_INTRANET_HOSTNAME_PATTERN = r'[a-z,0-9,-]{1,15}' """ - ul = '\u00a1-\uffff' # unicode letters range (must be a unicode string, not a raw string) + ul = "\u00a1-\uffff" # unicode letters range (must be a unicode string, not a raw string) # IP patterns - ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}' - ipv6_re = r'\[[0-9a-f:\.]+\]' # (simple regex, validated later) + ipv4_re = ( + r"(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}" + ) + ipv6_re = r"\[[0-9a-f:\.]+\]" # (simple regex, validated later) # Host patterns - hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]*[a-z' + ul + r'0-9])?' - domain_re = r'(?:\.[a-z' + ul + r'0-9]+(?:[a-z' + ul + r'0-9-]*[a-z' + ul + r'0-9]+)*)*' - tld_re = r'\.[a-z' + ul + r']{2,}\.?' - host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)' + hostname_re = r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]*[a-z" + ul + r"0-9])?" + domain_re = ( + r"(?:\.[a-z" + ul + r"0-9]+(?:[a-z" + ul + r"0-9-]*[a-z" + ul + r"0-9]+)*)*" + ) + tld_re = r"\.[a-z" + ul + r"]{2,}\.?" + host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)" def __init__(self, intranet_host_re=None, **kwargs): super().__init__(**kwargs) if intranet_host_re: self.host_re = ( - '(' + self.hostname_re + self.domain_re + self.tld_re + - '|' + intranet_host_re + '|localhost)' + "(" + + self.hostname_re + + self.domain_re + + self.tld_re + + "|" + + intranet_host_re + + "|localhost)" ) self.regex = re.compile( - r'^(?:[a-z0-9\.\-]*)://' - r'(?:\S+(?::\S*)?@)?' - r'(?:' + self.ipv4_re + '|' + self.ipv6_re + '|' + self.host_re + ')' - r'(?::\d{2,5})?' - r'(?:[/?#][^\s]*)?' - r'$', re.IGNORECASE) + r"^(?:[a-z0-9\.\-]*)://" + r"(?:\S+(?::\S*)?@)?" + r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")" + r"(?::\d{2,5})?" + r"(?:[/?#][^\s]*)?" + r"$", + re.IGNORECASE, + ) @deconstructible @@ -57,7 +68,7 @@ def __call__(self, value: str): if not isinstance(value, str) or len(value) > 100: raise ValidationError(self.message, code=self.code, params={"value": value}) - if not re.match(r'^[a-zA-Z0-9_\-]+$', value): + if not re.match(r"^[a-zA-Z0-9_\-]+$", value): raise ValidationError(self.message, code=self.code, params={"value": value}) return value @@ -65,7 +76,7 @@ def __call__(self, value: str): class ExtendedURLValidator(IntranetURLValidator): # Phone numbers don't match the host regex in Django's validator, # so we test for a simple alternative. - tel_re = r'^tel\:[0-9 \#\*\-\.\(\)\+]+$' + tel_re = r"^tel\:[0-9 \#\*\-\.\(\)\+]+$" def __init__(self, allowed_link_types: list = None, **kwargs): self.allowed_link_types = allowed_link_types @@ -77,15 +88,25 @@ def __call__(self, value: str): if self.unsafe_chars.intersection(value): raise ValidationError(self.message, code=self.code, params={"value": value}) # Check if just an anchor - if value.startswith("#") and (self.allowed_link_types is None or "anchor" in self.allowed_link_types): + if value.startswith("#") and ( + self.allowed_link_types is None or "anchor" in self.allowed_link_types + ): return AnchorValidator()(value) # Check if the scheme is valid. scheme = value.split(":")[0].lower() - if scheme == "tel" and (self.allowed_link_types is None or "tel" in self.allowed_link_types): + if scheme == "tel" and ( + self.allowed_link_types is None or "tel" in self.allowed_link_types + ): if re.match(self.tel_re, value): return else: - raise ValidationError(_("Enter a valid phone number"), code=self.code, params={"value": value}) - if scheme == "mailto" and (self.allowed_link_types is None or "mailto" in self.allowed_link_types): + raise ValidationError( + _("Enter a valid phone number"), + code=self.code, + params={"value": value}, + ) + if scheme == "mailto" and ( + self.allowed_link_types is None or "mailto" in self.allowed_link_types + ): return EmailValidator()(value[7:]) return super().__call__(value) diff --git a/tests/settings.py b/tests/settings.py index 394b4e48..249bbfa1 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -3,39 +3,42 @@ HELPER_SETTINGS = { - 'INSTALLED_APPS': [ - 'filer', - 'tests.utils', + "INSTALLED_APPS": [ + "filer", + "tests.utils", ], - 'CMS_LANGUAGES': { - 1: [{ - 'code': 'en', - 'name': 'English', - }] + "CMS_LANGUAGES": { + 1: [ + { + "code": "en", + "name": "English", + } + ] }, - 'LANGUAGE_CODE': 'en', - 'THUMBNAIL_PROCESSORS': ( - 'easy_thumbnails.processors.colorspace', - 'easy_thumbnails.processors.autocrop', - 'filer.thumbnail_processors.scale_and_crop_with_subject_location', - 'easy_thumbnails.processors.filters', + "LANGUAGE_CODE": "en", + "THUMBNAIL_PROCESSORS": ( + "easy_thumbnails.processors.colorspace", + "easy_thumbnails.processors.autocrop", + "filer.thumbnail_processors.scale_and_crop_with_subject_location", + "easy_thumbnails.processors.filters", ), - 'ALLOWED_HOSTS': ['localhost'], - 'DJANGOCMS_LINK_USE_SELECT2': True, - 'CMS_TEMPLATES': ( - ('page.html', 'Normal page'), - ('static_placeholder.html', 'Page with static placeholder'), + "ALLOWED_HOSTS": ["localhost"], + "DJANGOCMS_LINK_USE_SELECT2": True, + "CMS_TEMPLATES": ( + ("page.html", "Normal page"), + ("static_placeholder.html", "Page with static placeholder"), ), - 'FILE_UPLOAD_TEMP_DIR': mkdtemp(), - 'CMS_CONFIRM_VERSION4': True, - 'DJANGOCMS_LINKABLE_MODELS': ["utils.thirdpartymodel"], + "FILE_UPLOAD_TEMP_DIR": mkdtemp(), + "CMS_CONFIRM_VERSION4": True, + "DJANGOCMS_LINKABLE_MODELS": ["utils.thirdpartymodel"], } def run(): from app_helper import runner - runner.cms('djangocms_link') + runner.cms("djangocms_link") -if __name__ == '__main__': + +if __name__ == "__main__": run() diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index 26f15463..2844e6aa 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -137,9 +137,13 @@ def setUp(self): self.items = ( ThirdPartyModel.objects.create(name="First", path="/first", site_id=1), - ThirdPartyModel.objects.create(name="Second", path="/second", site=self.second_site), + ThirdPartyModel.objects.create( + name="Second", path="/second", site=self.second_site + ), ThirdPartyModel.objects.create(name="django CMS", path="/django-cms"), - ThirdPartyModel.objects.create(name="django CMS rocks", path="/django-cms-2"), + ThirdPartyModel.objects.create( + name="django CMS rocks", path="/django-cms-2" + ), ) def test_auto_config(self): @@ -208,22 +212,38 @@ def test_site_selector(self): # Specific site item if site_id == 1: self.assertIn( - {'id': 'utils.thirdpartymodel:1', 'text': 'First', 'url': '/first'}, - destinations["children"] + { + "id": "utils.thirdpartymodel:1", + "text": "First", + "url": "/first", + }, + destinations["children"], ) else: self.assertIn( - {'id': 'utils.thirdpartymodel:2', 'text': 'Second', 'url': '/second'}, - destinations["children"] + { + "id": "utils.thirdpartymodel:2", + "text": "Second", + "url": "/second", + }, + destinations["children"], ) # All-sites items self.assertIn( - {'id': 'utils.thirdpartymodel:3', 'text': 'django CMS', 'url': '/django-cms'}, - destinations["children"] + { + "id": "utils.thirdpartymodel:3", + "text": "django CMS", + "url": "/django-cms", + }, + destinations["children"], ) self.assertIn( - {'id': 'utils.thirdpartymodel:4', 'text': 'django CMS rocks', 'url': '/django-cms-2'}, - destinations["children"] + { + "id": "utils.thirdpartymodel:4", + "text": "django CMS rocks", + "url": "/django-cms-2", + }, + destinations["children"], ) def test_get_reference(self): @@ -242,7 +262,6 @@ def test_get_reference(self): class LinkEndpointMultiModelTestCase(CMSTestCase): def setUp(self): - LinkAdmin = admin.site._registry[Link] self.endpoint = admin_reverse(LinkAdmin.global_link_url_name) diff --git a/tests/test_fields.py b/tests/test_fields.py index 94f9f914..15b0f81a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -10,9 +10,9 @@ class LinkFieldTestCase(TestCase): def setUp(self): self.page = create_page( - title='django CMS is fun', - template='page.html', - language='en', + title="django CMS is fun", + template="page.html", + language="en", ) self.file = get_filer_file() @@ -26,7 +26,7 @@ class LinkForm(forms.Form): # Render widget self.assertIn( '