diff --git a/.editorconfig b/.editorconfig index 3495fd660..d174f174f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true -[*.{py}] +[*.py] line_length=119 indent_style = space indent_size = 4 @@ -22,7 +22,7 @@ indent_size = 4 indent_style = tab indent_size = 4 -[*.{rst}] +[*.rst] line_length=100 [*.{json,yml,css}] diff --git a/.travis.yml b/.travis.yml index 8cab9b4a7..4ea21820e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: python - + python: - 3.5 - 3.6 @@ -13,13 +13,13 @@ env: - DJANGOVER=django30 CMSVER=cms37 matrix: - exclude: + allow_failures: - python: 3.8 env: DJANGOVER=django21 CMSVER=cms36 - python: 3.8 env: DJANGOVER=django22 CMSVER=cms36 - python: 3.8 - env: DJANGOVER=django22 CMSVER=cms37 + env: DJANGOVER=django30 CMSVER=cms37 install: - pip install tox diff --git a/cmsplugin_cascade/__init__.py b/cmsplugin_cascade/__init__.py index 89bd40fe2..91e6997aa 100644 --- a/cmsplugin_cascade/__init__.py +++ b/cmsplugin_cascade/__init__.py @@ -19,6 +19,6 @@ 15. git commit -m 'Start with ' 16. git push """ -__version__ = "1.3" +__version__ = "1.3.7" default_app_config = 'cmsplugin_cascade.apps.CascadeConfig' diff --git a/cmsplugin_cascade/admin.py b/cmsplugin_cascade/admin.py index 8ad80f204..6dbcb9b9d 100644 --- a/cmsplugin_cascade/admin.py +++ b/cmsplugin_cascade/admin.py @@ -1,4 +1,6 @@ from urllib.parse import urlparse +import requests + from django.conf.urls import url from django.contrib import admin from django.contrib.sites.shortcuts import get_current_site @@ -7,6 +9,7 @@ from django.http import JsonResponse, HttpResponseForbidden, HttpResponseNotFound from django.urls import reverse from django.utils.translation import get_language_from_request + from cms.models.pagemodel import Page from cms.extensions import PageExtensionAdmin from cms.utils.page import get_page_from_path @@ -33,12 +36,14 @@ def get_form(self, request, obj=None, **kwargs): def get_urls(self): urls = [ - url(r'^get_page_sections/$', lambda: None, name='get_page_sections'), # just to reverse + url(r'^get_page_sections/$', lambda _: JsonResponse({'element_ids': []}), + name='get_page_sections'), # just to reverse url(r'^get_page_sections/(?P\d+)$', self.admin_site.admin_view(self.get_page_sections)), url(r'^published_pages/$', self.get_published_pagelist, name='get_published_pagelist'), url(r'^fetch_fonticons/(?P[0-9]+)$', self.fetch_fonticons), url(r'^fetch_fonticons/$', self.fetch_fonticons, name='fetch_fonticons'), + url(r'^validate_exturl/$', self.validate_exturl, name='validate_exturl'), ] urls.extend(super().get_urls()) return urls @@ -108,6 +113,19 @@ def fetch_fonticons(self, request, iconfont_id=None): data['families'] = icon_font.get_icon_families() return JsonResponse(data) + def validate_exturl(self, request): + """ + Perform a GET request onto the given external URL and return its status. + """ + exturl = request.GET.get('exturl') + request_headers = {'User-Agent': 'Django-CMS-Cascade'} + try: + response = requests.get(exturl, allow_redirects=True, headers=request_headers) + except Exception: + return JsonResponse({'status_code': 500}) + else: + return JsonResponse({'status_code': response.status_code}) + def changeform_view(self, request, object_id=None, form_url='', extra_context=None): extra_context = dict(extra_context or {}, icon_fonts=IconFont.objects.all()) return super().changeform_view( diff --git a/cmsplugin_cascade/app_settings.py b/cmsplugin_cascade/app_settings.py index 54464fca5..eed5dd198 100644 --- a/cmsplugin_cascade/app_settings.py +++ b/cmsplugin_cascade/app_settings.py @@ -1,5 +1,5 @@ -class AppSettings(object): +class AppSettings: def _setting(self, name, default=None): from django.conf import settings diff --git a/cmsplugin_cascade/bootstrap4/container.py b/cmsplugin_cascade/bootstrap4/container.py index 3b829a51a..1cebfac4f 100644 --- a/cmsplugin_cascade/bootstrap4/container.py +++ b/cmsplugin_cascade/bootstrap4/container.py @@ -5,6 +5,7 @@ from django.utils.safestring import mark_safe from django.utils.text import format_lazy from django.utils.translation import ngettext_lazy, gettext_lazy as _ + from cms.plugin_pool import plugin_pool from entangled.forms import EntangledModelFormMixin from cmsplugin_cascade import app_settings @@ -57,7 +58,7 @@ def clean_breapoints(self): return self.cleaned_data['glossary'] -class ContainerGridMixin(object): +class ContainerGridMixin: def get_grid_instance(self): fluid = self.glossary.get('fluid', False) try: @@ -124,7 +125,7 @@ class Meta: untangled_fields = ['num_children'] -class RowGridMixin(object): +class RowGridMixin: def get_grid_instance(self): row = grid.Bootstrap4Row() query = Q(plugin_type='BootstrapContainerPlugin') | Q(plugin_type='BootstrapColumnPlugin') \ @@ -156,7 +157,7 @@ def save_model(self, request, obj, form, change): plugin_pool.register_plugin(BootstrapRowPlugin) -class ColumnGridMixin(object): +class ColumnGridMixin: valid_keys = ['xs-column-width', 'sm-column-width', 'md-column-width', 'lg-column-width', 'xs-column-width', 'xs-column-offset', 'sm-column-offset', 'md-column-offset', 'lg-column-offset', 'xs-column-offset'] def get_grid_instance(self): @@ -195,7 +196,7 @@ def choose_help_text(*phrases): return phrases[1].format(bs4_breakpoints[first].min) else: return phrases[2] - + if 'parent' in self._cms_initial_attributes: container=self._cms_initial_attributes['parent'].get_ancestors().order_by('depth').last().get_bound_plugin() else: diff --git a/cmsplugin_cascade/bootstrap4/grid.py b/cmsplugin_cascade/bootstrap4/grid.py index a953e1124..7a004c5cc 100644 --- a/cmsplugin_cascade/bootstrap4/grid.py +++ b/cmsplugin_cascade/bootstrap4/grid.py @@ -4,6 +4,7 @@ import itertools from operator import add import re + from django.utils.translation import gettext_lazy as _ @@ -74,7 +75,7 @@ def media_query(self): ][self.value] -class Bound(object): +class Bound: def __init__(self, min, max): self.min = float(min) self.max = float(max) @@ -122,7 +123,7 @@ def extend(self, other): } -class Break(object): +class Break: def __init__(self, breakpoint, classes, narrower=None): self.breakpoint = breakpoint self.fixed_units = 0 diff --git a/cmsplugin_cascade/bootstrap4/jumbotron.py b/cmsplugin_cascade/bootstrap4/jumbotron.py index be349bce6..f1f9b6295 100644 --- a/cmsplugin_cascade/bootstrap4/jumbotron.py +++ b/cmsplugin_cascade/bootstrap4/jumbotron.py @@ -4,6 +4,7 @@ from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from entangled.forms import EntangledModelFormMixin + from cms.plugin_pool import plugin_pool from cmsplugin_cascade import app_settings from cmsplugin_cascade.fields import ColorField, MultiSizeField, CascadeImageField @@ -16,7 +17,7 @@ logger = logging.getLogger('cascade') -class ImageBackgroundMixin(object): +class ImageBackgroundMixin: @property def element_heights(self): element_heights = self.glossary.get('element_heights', {}) @@ -220,7 +221,7 @@ def render(self, context, instance, placeholder): 'CSS_PREFIXES': app_settings.CSS_PREFIXES, }) except Exception as exc: - logger.warning("Unable generate picture elements. Reason: {}".format(exc)) + logger.warning("Unable generate picture elements. Reason: {}".format(exc)) return self.super(BootstrapJumbotronPlugin, self).render(context, instance, placeholder) @classmethod diff --git a/cmsplugin_cascade/clipboard/cms_plugins.py b/cmsplugin_cascade/clipboard/cms_plugins.py index 96de225c2..f6f9b9ec7 100644 --- a/cmsplugin_cascade/clipboard/cms_plugins.py +++ b/cmsplugin_cascade/clipboard/cms_plugins.py @@ -121,7 +121,7 @@ def paste_from_clipboard(self, request, form): cascade_clipboard.save(update_fields=['last_accessed_at']) # detach plugins from clipboard and reattach them to current placeholder - cb_placeholder_plugin = request.toolbar.clipboard.cmsplugin_set.first() + cb_placeholder_plugin = request.toolbar.clipboard.cmsplugin_set.filter(plugin_type='PlaceholderPlugin').first() cb_placeholder_instance, _ = cb_placeholder_plugin.get_plugin_instance() new_plugins = cb_placeholder_instance.placeholder_ref.get_plugins() new_plugins.update(placeholder=placeholder) diff --git a/cmsplugin_cascade/extra_fields/config.py b/cmsplugin_cascade/extra_fields/config.py index 122abd9b3..1c930f109 100644 --- a/cmsplugin_cascade/extra_fields/config.py +++ b/cmsplugin_cascade/extra_fields/config.py @@ -1,5 +1,5 @@ -class PluginExtraFieldsConfig(object): +class PluginExtraFieldsConfig: """ Each Cascade Plugin can be configured to accept extra fields, such as an ID tag, one or more CSS classes or inlines styles. It is possible to configure these fields globally using an diff --git a/cmsplugin_cascade/fields.py b/cmsplugin_cascade/fields.py index f99d86915..24011a80c 100644 --- a/cmsplugin_cascade/fields.py +++ b/cmsplugin_cascade/fields.py @@ -11,13 +11,14 @@ from django.core.validators import ProhibitNullCharactersValidator from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _, pgettext + from cmsplugin_cascade import app_settings from cmsplugin_cascade.widgets import ColorPickerWidget, BorderChoiceWidget, MultipleTextInputWidget from filer.fields.image import FilerImageField, AdminImageFormField from filer.settings import settings as filer_settings -class GlossaryField(object): +class GlossaryField: """ Deprecated. Behave similar to django.forms.Field, encapsulating a partial dictionary, stored as diff --git a/cmsplugin_cascade/forms.py b/cmsplugin_cascade/forms.py index 5681dac69..3d68b4bb7 100644 --- a/cmsplugin_cascade/forms.py +++ b/cmsplugin_cascade/forms.py @@ -1,5 +1,10 @@ +from django.forms.formsets import DELETION_FIELD_NAME +from django.forms.models import ModelForm -class ManageChildrenFormMixin(object): +from entangled.forms import EntangledModelFormMixin, EntangledModelForm + + +class ManageChildrenFormMixin: """ Classes derived from ``CascadePluginBase`` can optionally add this mixin class to their form, offering the input field ``num_children`` in their plugin editor. The content of this field is @@ -15,3 +20,21 @@ def __init__(self, *args, **kwargs): initial = {'num_children': instance.get_num_children()} kwargs.update(initial=initial) super().__init__(*args, **kwargs) + + +class CascadeModelFormMixin(EntangledModelFormMixin): + """ + Classes inheriting from InlineAdmin and defining their own `form` shall use this special + variant of an `EntangledModelForm`, otherwise deletion of inlined elements does not work. + """ + def _clean_form(self): + internal_fields = ['cascade_element', 'id', DELETION_FIELD_NAME] + internal_cleaned_data = {key: val for key, val in self.cleaned_data.items() if key in internal_fields} + super()._clean_form() + self.cleaned_data.update(internal_cleaned_data) + + +class CascadeModelForm(CascadeModelFormMixin, ModelForm): + """ + A convenience class to create a ModelForms to be used with djangocms-cascade + """ diff --git a/cmsplugin_cascade/generic/mixins.py b/cmsplugin_cascade/generic/mixins.py index e6e69c3a6..53fbee448 100644 --- a/cmsplugin_cascade/generic/mixins.py +++ b/cmsplugin_cascade/generic/mixins.py @@ -2,6 +2,7 @@ from django.forms.fields import CharField from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ + from entangled.forms import EntangledModelFormMixin from cmsplugin_cascade import app_settings from cmsplugin_cascade.models import CascadePage @@ -41,7 +42,7 @@ def check_unique_element_id(cls, instance, element_id): raise ValidationError(msg.format(element_id)) -class SectionModelMixin(object): +class SectionModelMixin: def element_id(self): id_attr = self.glossary.get('element_id') if id_attr: @@ -57,7 +58,7 @@ def delete(self, *args, **kwargs): super().delete(*args, **kwargs) -class SectionMixin(object): +class SectionMixin: def get_form(self, request, obj=None, **kwargs): form = kwargs.get('form', self.form) assert issubclass(form, EntangledModelFormMixin), "Form must inherit from EntangledModelFormMixin" diff --git a/cmsplugin_cascade/generic/text_image.py b/cmsplugin_cascade/generic/text_image.py index 8ce0408e4..e6d784c4a 100644 --- a/cmsplugin_cascade/generic/text_image.py +++ b/cmsplugin_cascade/generic/text_image.py @@ -63,8 +63,7 @@ class TextImagePlugin(LinkPluginBase): allow_children = False require_parent = False form = type('TextImageForm', (LinkFormMixin, TextImageFormMixin), {'require_link': False}) - html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'} - html_tag_attributes.update(LinkPluginBase.html_tag_attributes) + html_tag_attributes = {'image_title': 'title', 'alt_tag': 'alt'} class Media: js = ['admin/js/jquery.init.js', 'cascade/js/admin/textimageplugin.js'] diff --git a/cmsplugin_cascade/hide_plugins.py b/cmsplugin_cascade/hide_plugins.py index edc93a4e8..d73adb707 100644 --- a/cmsplugin_cascade/hide_plugins.py +++ b/cmsplugin_cascade/hide_plugins.py @@ -2,6 +2,7 @@ from django.forms.fields import BooleanField from django.utils.translation import gettext_lazy as _ from django.template import engines + from entangled.forms import EntangledModelFormMixin @@ -16,7 +17,7 @@ class Meta: entangled_fields = {'glossary': ['hide_plugin']} -class HidePluginMixin(object): +class HidePluginMixin: """ This mixin class adds a checkbox to each named plugin, which if checked hides that plugin during the rendering phase. diff --git a/cmsplugin_cascade/icon/plugin_base.py b/cmsplugin_cascade/icon/plugin_base.py index 23b6e58aa..20eece6c6 100644 --- a/cmsplugin_cascade/icon/plugin_base.py +++ b/cmsplugin_cascade/icon/plugin_base.py @@ -1,7 +1,7 @@ from django.utils.safestring import mark_safe from cmsplugin_cascade.models import IconFont from cmsplugin_cascade.plugin_base import CascadePluginMixinBase -from entangled.forms import get_related_object +from entangled.utils import get_related_object class IconPluginMixin(CascadePluginMixinBase): diff --git a/cmsplugin_cascade/image.py b/cmsplugin_cascade/image.py index 16b4f7f93..05c83e38e 100644 --- a/cmsplugin_cascade/image.py +++ b/cmsplugin_cascade/image.py @@ -1,7 +1,8 @@ from django.forms.fields import CharField from django.utils.translation import gettext_lazy as _ -from entangled.forms import EntangledModelFormMixin, EntangledField, get_related_object +from entangled.forms import EntangledModelFormMixin, EntangledField +from entangled.utils import get_related_object from cmsplugin_cascade.fields import CascadeImageField diff --git a/cmsplugin_cascade/leaflet/map.py b/cmsplugin_cascade/leaflet/map.py index c0e0a471e..760a22ee0 100644 --- a/cmsplugin_cascade/leaflet/map.py +++ b/cmsplugin_cascade/leaflet/map.py @@ -1,14 +1,13 @@ import json -from django.forms.fields import CharField, BooleanField from django.forms import widgets +from django.forms.fields import CharField, BooleanField from django.db.models.fields.related import ManyToOneRel from django.contrib.admin import StackedInline from django.core.exceptions import ValidationError from django.utils.html import strip_tags, strip_spaces_between_tags from django.utils.safestring import mark_safe from django.utils.translation import ngettext_lazy, gettext_lazy as _ -from entangled.forms import EntangledModelFormMixin, EntangledModelForm from filer.fields.image import FilerImageField, AdminImageFormField from filer.settings import settings as filer_settings from filer.utils.loader import load_model @@ -17,6 +16,7 @@ from cmsplugin_cascade import app_settings from cmsplugin_cascade.fields import HiddenDictField, SizeField, MultiSizeField +from cmsplugin_cascade.forms import CascadeModelForm, CascadeModelFormMixin from cmsplugin_cascade.image import ImagePropertyMixin from cmsplugin_cascade.mixins import WithInlineElementsMixin from cmsplugin_cascade.models import InlineCascadeElement @@ -32,7 +32,7 @@ def data(self): return mark_safe(json.dumps(self.glossary)) -class MarkerForm(EntangledModelForm): +class MarkerForm(CascadeModelForm): title = CharField( label=_("Marker Title"), widget=widgets.TextInput(attrs={'size': 60}), @@ -111,7 +111,7 @@ class MarkerInline(StackedInline): extra = 0 -class LeafletFormMixin(EntangledModelFormMixin): +class LeafletFormMixin(CascadeModelFormMixin): map_width = SizeField( label=_("Map Width"), allowed_units=['px', '%'], diff --git a/cmsplugin_cascade/leaflet/settings.py b/cmsplugin_cascade/leaflet/settings.py index 6ba09ae60..ee4ff3556 100644 --- a/cmsplugin_cascade/leaflet/settings.py +++ b/cmsplugin_cascade/leaflet/settings.py @@ -6,7 +6,9 @@ def set_defaults(config): config.setdefault('leaflet', {}) config['leaflet'].setdefault('tilesURL', 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'), config['leaflet'].setdefault('default_position', {'lat': 30.0, 'lng': -40.0, 'zoom': 3}) - config['leaflet'].setdefault('id', 'mapbox.streets'), + config['leaflet'].setdefault('id', 'mapbox/streets-v11'), config['leaflet'].setdefault('maxZoom', 18), + config['leaflet'].setdefault('tileSize', 512) + config['leaflet'].setdefault('zoomOffset', -1) config['leaflet'].setdefault('detectRetina', True) config['leaflet'].setdefault('attribution', mark_safe('Map data © OpenStreetMap')), diff --git a/cmsplugin_cascade/link/forms.py b/cmsplugin_cascade/link/forms.py index 04e447fed..09575115a 100644 --- a/cmsplugin_cascade/link/forms.py +++ b/cmsplugin_cascade/link/forms.py @@ -1,5 +1,3 @@ -import re -import requests from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.contrib.admin.sites import site as admin_site from django.db.models.fields.related import ManyToOneRel @@ -12,7 +10,8 @@ from cms.utils import get_current_site from cms.models import Page -from entangled.forms import EntangledModelFormMixin, get_related_object +from entangled.forms import EntangledModelFormMixin +from entangled.utils import get_related_object from filer.models.filemodels import File as FilerFileModel from filer.fields.file import AdminFileWidget, FilerFileField @@ -185,48 +184,35 @@ def _preset_section(self, instance): except (AttributeError, ObjectDoesNotExist): pass - def clean(self): - cleaned_data = super().clean() - link_type = cleaned_data.get('link_type') + def _post_clean(self): + super()._post_clean() error = None + empty_fields = [None, ''] + link_type = self.cleaned_data['glossary'].get('link_type') if link_type == 'cmspage': - if not cleaned_data.get('cms_page'): - error = ValidationError(_("CMS page to link to is missing.")) + if self.cleaned_data['glossary'].get('cms_page', False) in empty_fields: + error = ValidationError(_("CMS page to link to is missing."), code='required') self.add_error('cms_page', error) elif link_type == 'download': - if not cleaned_data.get('download_file'): - error = ValidationError(_("File for download is missing.")) + if self.cleaned_data['glossary'].get('download_file', False) in empty_fields: + error = ValidationError(_("File for download is missing."), code='required') self.add_error('download_file', error) elif link_type == 'exturl': - ext_url = cleaned_data.get('ext_url') - if ext_url: - try: - response = requests.head(ext_url, allow_redirects=True) - if response.status_code != 200: - error = ValidationError(_("No external page found on {url}.").format(url=ext_url)) - except Exception as exc: - error = ValidationError(_("Failed to connect to {url}.").format(url=ext_url)) - else: - error = ValidationError(_("No valid URL provided.")) - if error: + ext_url = self.cleaned_data['glossary'].get('ext_url', False) + if ext_url in empty_fields: + error = ValidationError(_("No valid URL provided."), code='required') self.add_error('ext_url', error) elif link_type == 'email': - mail_to = cleaned_data.get('mail_to') - if mail_to: - if not re.match(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)', mail_to): - msg = _("'{email}' is not a valid email address.") - error = ValidationError(msg.format(email=mail_to)) - else: - error = ValidationError(_("No email address provided.")) - if error: + if self.cleaned_data['glossary'].get('mail_to', False) in empty_fields: + error = ValidationError(_("No email address provided."), code='required') self.add_error('mail_to', error) elif link_type == 'phonenumber': - phone_number = cleaned_data.get('phone_number') - if phone_number: - cleaned_data['phone_number'] = str(phone_number) - if error: - raise error - return cleaned_data + if self.cleaned_data['glossary'].get('phone_number', False) in empty_fields: + error = ValidationError(_("No phone number provided."), code='required') + self.add_error('phone_number', error) + + def clean_phone_number(self): + return str(self.cleaned_data['phone_number']) @classmethod def unset_required_for(cls, sharable_fields): diff --git a/cmsplugin_cascade/link/plugin_base.py b/cmsplugin_cascade/link/plugin_base.py index bb8739bc0..b66ad7cc8 100644 --- a/cmsplugin_cascade/link/plugin_base.py +++ b/cmsplugin_cascade/link/plugin_base.py @@ -1,7 +1,8 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.functional import cached_property from django.utils.safestring import mark_safe -from entangled.forms import get_related_object + +from entangled.utils import get_related_object from cmsplugin_cascade.plugin_base import CascadePluginBase from filer.models.filemodels import File as FilerFileModel @@ -12,9 +13,10 @@ class LinkPluginBase(CascadePluginBase): require_parent = False ring_plugin = 'LinkPluginBase' raw_id_fields = ['download_file'] - html_tag_attributes = {'title': 'title', 'target': 'target'} + html_tag_attributes = {'link_title': 'title', 'link_target': 'target'} class Media: + css = {'all': ['cascade/css/admin/linkplugin.css']} js = ['admin/js/jquery.init.js', 'cascade/js/admin/linkplugin.js'] @classmethod @@ -53,7 +55,7 @@ class DefaultLinkPluginBase(LinkPluginBase): ring_plugin = 'LinkPluginBase' -class LinkElementMixin(object): +class LinkElementMixin: """ A mixin class to convert a CascadeElement into a proxy model for rendering the ```` element. Note that a Link inside the Text Editor Plugin is rendered using ``str(instance)`` rather diff --git a/cmsplugin_cascade/migrations/0027_version_1.py b/cmsplugin_cascade/migrations/0027_version_1.py index a21f06754..7337b3ee7 100644 --- a/cmsplugin_cascade/migrations/0027_version_1.py +++ b/cmsplugin_cascade/migrations/0027_version_1.py @@ -22,17 +22,16 @@ def foreign_key(): elif link['type'] == 'exturl': glossary.update({ 'link_type': 'exturl', - 'ext_url': foreign_key(), + 'ext_url': link['url'], }) elif link['type'] == 'email': glossary.update({ 'link_type': 'email', - 'mail_to': foreign_key(), + 'mail_to': link['mail_to'], }) elif link['type'] and link['type'] != 'none': glossary.update({ 'link_type': link['type'], - link['type']: foreign_key(), }) else: glossary.update({ diff --git a/cmsplugin_cascade/plugin_base.py b/cmsplugin_cascade/plugin_base.py index 0150758bd..357c31c7a 100644 --- a/cmsplugin_cascade/plugin_base.py +++ b/cmsplugin_cascade/plugin_base.py @@ -127,7 +127,7 @@ def __new__(cls, name, bases, attrs): return super().__new__(cls, name, bases, attrs) -class TransparentWrapper(object): +class TransparentWrapper: """ Add this mixin class to other Cascade plugins, wishing to be added transparently between other plugins restricting parent-children relationships. diff --git a/cmsplugin_cascade/segmentation/mixins.py b/cmsplugin_cascade/segmentation/mixins.py index fd993d33a..fdf2fbc61 100644 --- a/cmsplugin_cascade/segmentation/mixins.py +++ b/cmsplugin_cascade/segmentation/mixins.py @@ -6,10 +6,11 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _, ngettext from django.utils.html import format_html + from cms.constants import REFRESH_PAGE -class SegmentPluginModelMixin(object): +class SegmentPluginModelMixin: """ TODO: whenever cmsplugin_cascade drops support for django-CMS < 3.4, this mixin class shall be added to the plugin rather than to the model @@ -47,7 +48,7 @@ def get_context_override(self, request): return context_override -class EmulateUserAdminMixin(object): +class EmulateUserAdminMixin: UserModel = get_user_model() @staticmethod diff --git a/cmsplugin_cascade/sphinx/cms_menus.py b/cmsplugin_cascade/sphinx/cms_menus.py index 79082c61a..0905f4529 100644 --- a/cmsplugin_cascade/sphinx/cms_menus.py +++ b/cmsplugin_cascade/sphinx/cms_menus.py @@ -12,7 +12,7 @@ class DocumentationMenu(CMSAttachMenu): name = _("Documentation Menu") # give the menu a name this is required. - def get_nodes(self, request): + def get_nodes(self, request, root_page): """ This method is used to build the menu tree. """ diff --git a/cmsplugin_cascade/sphinx/theme/bootstrap-fragments/layout.html b/cmsplugin_cascade/sphinx/theme/bootstrap-fragments/layout.html index 835402965..fb3813c1e 100644 --- a/cmsplugin_cascade/sphinx/theme/bootstrap-fragments/layout.html +++ b/cmsplugin_cascade/sphinx/theme/bootstrap-fragments/layout.html @@ -1,13 +1,5 @@ {% set bootstrap_version, navbar_version = "3.3.7", "" %} -{% set script_files = script_files + [ - '_static/js/jquery-1.11.0.min.js', - '_static/js/jquery-fix.js', - '_static/bootstrap-' + bootstrap_version + '/js/bootstrap.min.js', - '_static/bootstrap-sphinx.js' - ] -%} - {%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and sidebars %} {%- set bs_content_width = render_sidebar and "9" or "12"%} @@ -35,13 +27,12 @@ {% block sidebarsourcelink %}{% endblock %} {%- block content %} -
-
- {%- block sidebar1 %}{{ bsidebar() }}{%- endblock %} -
- {% block body %}{% endblock %} -
-
+
+ {%- block sidebar1 %}{%- endblock %} +
+ {% block body %}{% endblock %} +
+ {%- block sidebar2 %}{{ bsidebar() }}{%- endblock %}
{%- endblock %} diff --git a/cmsplugin_cascade/static/cascade/css/admin/linkplugin.css b/cmsplugin_cascade/static/cascade/css/admin/linkplugin.css new file mode 100644 index 000000000..51ab0278a --- /dev/null +++ b/cmsplugin_cascade/static/cascade/css/admin/linkplugin.css @@ -0,0 +1,13 @@ +#id_ext_url { + background-position-x: calc(100% - 8px); + background-position-y: 50%; + background-clip: padding-box; + background-size: 24px; + background-repeat: no-repeat; +} +#id_ext_url.valid { + background-image: url('data:image/svg+xml;utf8,') !important; +} +#id_ext_url.invalid { + background-image: url('data:image/svg+xml;utf8,'); +} \ No newline at end of file diff --git a/cmsplugin_cascade/static/cascade/js/admin/cascadepage.js b/cmsplugin_cascade/static/cascade/js/admin/cascadepage.js index 1e366cb28..b7ac95ad9 100644 --- a/cmsplugin_cascade/static/cascade/js/admin/cascadepage.js +++ b/cmsplugin_cascade/static/cascade/js/admin/cascadepage.js @@ -46,7 +46,7 @@ django.jQuery(function($) { lis.push('
  • '); }); $symbol.before('
      ' + lis.join('') + '
    '); - $('#fonticon_search_query').bind('keyup paste', function(event) { + $('#fonticon_search_query').on('keyup paste', function(event) { var fonticon_symbols = $('#fonticon_symbols').find('li'), re; if (event.target.value) { re = new RegExp(event.target.value, 'i'); diff --git a/cmsplugin_cascade/static/cascade/js/admin/iconpluginmixin.js b/cmsplugin_cascade/static/cascade/js/admin/iconpluginmixin.js index f309601e2..b103894fe 100644 --- a/cmsplugin_cascade/static/cascade/js/admin/iconpluginmixin.js +++ b/cmsplugin_cascade/static/cascade/js/admin/iconpluginmixin.js @@ -60,7 +60,7 @@ django.jQuery(function($) { lis.push('
  • '); }); $symbol.before('
      ' + lis.join('') + '
    '); - $('#fonticon_search_query').bind('keyup paste', function(event) { + $('#fonticon_search_query').on('keyup paste', function(event) { var fonticon_symbols = $('#fonticon_symbols').find('li'), re; if (event.target.value) { re = new RegExp(event.target.value, 'i'); diff --git a/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js b/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js index e5b6e50a0..21727bd55 100644 --- a/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js +++ b/cmsplugin_cascade/static/cascade/js/admin/linkplugin.js @@ -1,6 +1,7 @@ django.jQuery(function($) { 'use strict'; var $link_type = $("#id_link_type"), $cmspage_select = $("#id_cms_page"); + var $link_ext_url = $('#id_ext_url'); var $link_target = $(".form-row.field-link_target"); var $link_title = $(".form-row.field-link_title"); @@ -34,6 +35,9 @@ django.jQuery(function($) { $cmspage_select.change(function(evt) { self.toggleCMSPage(evt.target.value); }); + $link_ext_url.on('blur', function(evt) { + self.validateExtUrl(evt.target.value); + }); this.refreshChangeForm(); }, initializeLinkTypes: function() { @@ -73,8 +77,20 @@ django.jQuery(function($) { $selSection.val(null); }); }, + validateExtUrl: function(exturl) { + $.get(django.cascade.validate_exturl_url, { + exturl: exturl, + }, function(response) { + if (response.status_code === 200) { + $link_ext_url.addClass('valid').removeClass('invalid'); + } else { + $link_ext_url.addClass('invalid').removeClass('valid'); + } + }); + }, refreshChangeForm: function() { this.toggleLinkTypes($link_type.val()); + this.validateExtUrl($link_ext_url.val()); this.$super && this.$super(); } }); diff --git a/cmsplugin_cascade/strides.py b/cmsplugin_cascade/strides.py index e0c11991b..98f216532 100644 --- a/cmsplugin_cascade/strides.py +++ b/cmsplugin_cascade/strides.py @@ -15,7 +15,7 @@ __all__ = ['register_stride', 'StrideContentRenderer'] -class EmulateQuerySet(object): +class EmulateQuerySet: def __init__(self, elements): self.elements = elements @@ -24,7 +24,7 @@ def all(self): yield type(str('StrideInlineElement'), (object,), {'id': id, 'glossary': glossary})() -class StrideElementBase(object): +class StrideElementBase: """ Emulate a CascadeElement to be used by the CascadeContentRenderer instead of the CMSContentRenderer. """ @@ -91,7 +91,7 @@ def html_tag_attributes(self): return '' -class TextStrideElement(object): +class TextStrideElement: def __init__(self, plugin, data, children_data, parent=None): self.plugin = plugin self.pk = data.get('pk') @@ -181,7 +181,7 @@ def render(self, context, instance, placeholder): return context -class StrideContentRenderer(object): +class StrideContentRenderer: def __init__(self, request): self.request = request self.language = get_language_from_request(request) diff --git a/cmsplugin_cascade/templates/cascade/admin/change_form.html b/cmsplugin_cascade/templates/cascade/admin/change_form.html index 05a77c4c6..852c3f70a 100644 --- a/cmsplugin_cascade/templates/cascade/admin/change_form.html +++ b/cmsplugin_cascade/templates/cascade/admin/change_form.html @@ -31,6 +31,7 @@

    {% trans "There are no further settings for this plugin" %}

    '{{ icof.id }}': '{{ icof.get_stylesheet_url }}'{% if not forloop.last %},{% endif %}{% endfor %} }; django.cascade.fetch_fonticons_url = "{% url 'admin:fetch_fonticons' %}"; + django.cascade.validate_exturl_url = "{% url 'admin:validate_exturl' %}"; {% endif %} {% endblock %} diff --git a/cmsplugin_cascade/templates/cascade/link/.editorconfig b/cmsplugin_cascade/templates/cascade/link/.editorconfig new file mode 100644 index 000000000..fa117062d --- /dev/null +++ b/cmsplugin_cascade/templates/cascade/link/.editorconfig @@ -0,0 +1,2 @@ +[*.html] +insert_final_newline = false diff --git a/cmsplugin_cascade/templates/cascade/link/link-base-nostyle.html b/cmsplugin_cascade/templates/cascade/link/link-base-nostyle.html index 1fe146ddf..8d6e25c15 100644 --- a/cmsplugin_cascade/templates/cascade/link/link-base-nostyle.html +++ b/cmsplugin_cascade/templates/cascade/link/link-base-nostyle.html @@ -2,4 +2,4 @@ {% block link_link %} {% if instance_link %}
    {% endif %}{% block link_content %}{{ instance.content }}{% endblock %}{% if instance_link %}{% endif %} {% endblock %} -{% endwith %}{% endspaceless %} +{% endwith %}{% endspaceless %}{# NEVER ADD A FINAL NEWLINE #} \ No newline at end of file diff --git a/cmsplugin_cascade/templates/cascade/link/link-base.html b/cmsplugin_cascade/templates/cascade/link/link-base.html index 9ebc4646d..a1f7f7758 100644 --- a/cmsplugin_cascade/templates/cascade/link/link-base.html +++ b/cmsplugin_cascade/templates/cascade/link/link-base.html @@ -2,4 +2,4 @@ {% block link_link %} {% if instance_link %}{% endif %}{% block link_content %}{{ instance.content }}{% endblock %}{% if instance_link %}{% endif %} {% endblock %} -{% endwith %}{% endspaceless %} +{% endwith %}{% endspaceless %}{# NEVER ADD A FINAL NEWLINE #} \ No newline at end of file diff --git a/cmsplugin_cascade/templates/cascade/link/text-link-linebreak.html b/cmsplugin_cascade/templates/cascade/link/text-link-linebreak.html index 8b46ff3f9..90fb0a539 100644 --- a/cmsplugin_cascade/templates/cascade/link/text-link-linebreak.html +++ b/cmsplugin_cascade/templates/cascade/link/text-link-linebreak.html @@ -1 +1 @@ -{% include "cascade/link/text-link.html" %}
    \ No newline at end of file +{% include "cascade/link/text-link.html" %}
    diff --git a/cmsplugin_cascade/templates/cascade/link/text-link.html b/cmsplugin_cascade/templates/cascade/link/text-link.html index 13fb814c4..aa4f00ce4 100644 --- a/cmsplugin_cascade/templates/cascade/link/text-link.html +++ b/cmsplugin_cascade/templates/cascade/link/text-link.html @@ -1 +1 @@ -{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %}{% block link_content %}{{ instance.content }}{% endblock %}{% endwith %} +{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %}{% block link_content %}{{ instance.content }}{% endblock %}{% endwith %} \ No newline at end of file diff --git a/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html b/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html index 5c1f88396..41897cf4a 100644 --- a/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html +++ b/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html @@ -3,11 +3,11 @@ {% spaceless %} {% with instance_link=instance.link tag_type=instance.tag_type css_classes=instance.css_classes inline_styles=instance.inline_styles %} {% if instance_link %} - + {% elif css_classes or inline_styles %} {% endif %} {% if instance_link %}{% elif css_classes or inline_styles %}{% endif %} {% endwith %} -{% endspaceless %} \ No newline at end of file +{% endspaceless %} diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index bbc989bd2..05a1c18b4 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -2,6 +2,51 @@ Release History =============== +1.3.7 +===== +* Perform validation of external URL during editing, instead of form validation. Do not reject + invalid external URLs, just warn about them. + + +1.3.6 +===== +* Fix regression on link validation introduced in 1.3.5. + + +1.3.5 +===== +* In plugins inheriting from ``LinkPluginBase``, refactor validation from method ``clean()`` to + ``_post_clean()``, in order to avoid unjustified validation errors. +* Fix rare error when pasting from clipboard into structure view. +* Remove empty final line in templates used to render links; they added unwanted whitespace to + output. +* In some JS files: Replace deprecated jQuery `.bind()` call with `.on()`. +* Removed some legacy code dating back to Python2. + + +1.3.4 +===== +* Fix: Internal Server error raised when deleting content of link to CMS page's form field. +* When testing external links, use request with `User-Agent: Django-CMS-Cascade` instead of the + default. + + +1.3.3 +===== +* Fix: Deletion of markers in map plugin failed. + + +1.3.2 +===== +* Fix migration ``0027_version_1.py`` to migrate links from version<1 upwards. + + +1.3.1 +===== +* Fix external requiremnts. +* Replace deprecated ugettext against gettext. + + 1.3 === * Drop support for Python 2. @@ -183,7 +228,7 @@ Release History whatever is parsable. * Add ``role="button"`` to the **ButtonPlugin**. * Optionally add CSS class ``stretched-link`` introduced in Bootstrap-4.3 to ````. -* Fix: We can not see the SVG file, if the image file existed and was not found, specifically +* Fix: We can not see the SVG file, if the image file existed and was not found, specifically when copying a Persisted clipboard. * Fix: If jsonfield is serialized as string, convert and reload as JSON. * Fix: **ImagePlugin** / **PicturePlugin** can't be copied by clipboard CMS. diff --git a/docs/source/impatient.rst b/docs/source/impatient.rst index 83b31018d..6e151772d 100644 --- a/docs/source/impatient.rst +++ b/docs/source/impatient.rst @@ -17,7 +17,6 @@ Dependency packaging to made easy with Pipenv or Poetry. .. code-block:: bash - $ git clone --depth=1 https://github.com/jrief/djangocms-cascade.git $ cd djangocms-cascade/examples/ $ python -m venv .venv diff --git a/docs/source/sphinx.rst b/docs/source/sphinx.rst index e7e90a611..3d2850c01 100644 --- a/docs/source/sphinx.rst +++ b/docs/source/sphinx.rst @@ -30,14 +30,14 @@ To the project's ``settings.py``, add these options to the configuration directi ] CMS_TEMPLATES = [ - ..., + ... ('path/to/documentation.html', "Documentation Page"), ... ] SPHINX_DOCS_ROOT = '/path/to/docs/_build/fragments' -Replace ``'path/to/documentation.html'`` with a filename pointing to your documentation +Replace ``'/path/to/documentation.html'`` with a filename pointing to your documentation root template (see below). Point ``SPHINX_DOCS_ROOT`` onto the directory, into which the HTML page fragments are generated. @@ -46,15 +46,6 @@ Point ``SPHINX_DOCS_ROOT`` onto the directory, into which the HTML page fragment Configure Sphinx Builder ------------------------ -Locate the file ``Makefile`` inside the ``docs`` folder and add another target to it: - -.. code-block:: makefile - - fragments: - $(SPHINXBUILD) -b fragments $(ALLSPHINXOPTS) $(BUILDDIR)/fragments - @echo - @echo "Build finished. The HTML fragments are in $(BUILDDIR)/fragments." - Locate the file ``conf.py`` and add: .. code-block:: python @@ -65,7 +56,7 @@ Locate the file ``conf.py`` and add: ] By invoking ``make fragments``, Sphinx generates a HTML fragment for each page inside the -documentation folder, typically into ``docs/_build/fragments``. Later we use these fragments +documentation folder, typically into ``docs/build/fragments``. Later we use these fragments and include them using a normal Django view. diff --git a/setup.py b/setup.py index 9d0f8f1be..b02c693fd 100644 --- a/setup.py +++ b/setup.py @@ -16,10 +16,9 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', ] setup( @@ -31,10 +30,10 @@ url='https://github.com/jrief/djangocms-cascade', packages=find_packages(exclude=['examples', 'docs', 'tests']), install_requires=[ - 'django>=1.11,<3.0', - 'django-classy-tags>=0.8', + 'django>=2.1,<3.1', + 'django-classy-tags>=1.0', 'django-cms>=3.5,<4', - 'django-entangled', + 'django-entangled>=0.4', 'djangocms-text-ckeditor>=3.7', 'jsonfield', 'requests', diff --git a/tests/bootstrap4/test_accordion.py b/tests/bootstrap4/test_accordion.py index bef9c5fdf..9b4a124f4 100644 --- a/tests/bootstrap4/test_accordion.py +++ b/tests/bootstrap4/test_accordion.py @@ -2,7 +2,6 @@ from django.template.context import RequestContext from cms.api import add_plugin from cms.plugin_rendering import ContentRenderer -from django.utils.html import strip_spaces_between_tags, strip_tags from cms.utils.plugins import build_plugin_tree from cmsplugin_cascade.models import CascadeElement from cmsplugin_cascade.bootstrap4.accordion import BootstrapAccordionGroupPlugin, BootstrapAccordionPlugin diff --git a/tests/test_base.py b/tests/test_base.py index 206eb138b..0cdc6ad31 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -2,15 +2,16 @@ from __future__ import unicode_literals from django.contrib import admin +from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import make_password from django.template.context import Context + from cms.api import create_page from cms.test_utils.testcases import CMSTestCase from cmsplugin_cascade.models import CascadePage -from djangocms_helper.base_test import BaseTestCase - -class CascadeTestCase(CMSTestCase, BaseTestCase): +class CascadeTestCase(CMSTestCase): home_page = None def setUp(self): @@ -25,6 +26,22 @@ def setUp(self): self.request = self.get_request(self.home_page, 'en') self.admin_site = admin.sites.AdminSite() + UserModel = get_user_model() + UserModel.objects.get_or_create( + username='admin', + is_staff=True, + is_superuser=True, + is_active=True, + password=make_password('admin'), + ) + UserModel.objects.get_or_create( + username='staff', + is_staff=True, + is_superuser=False, + is_active=True, + password=make_password('staff'), + ) + def get_request_context(self): context = {} context['request'] = self.request diff --git a/tests/test_iconfont.py b/tests/test_iconfont.py index 5a7ca58a6..382461a6d 100644 --- a/tests/test_iconfont.py +++ b/tests/test_iconfont.py @@ -14,6 +14,7 @@ from cmsplugin_cascade.models import CascadeElement, IconFont from cmsplugin_cascade.icon.forms import IconFormMixin from cmsplugin_cascade.icon.simpleicon import SimpleIconPlugin +from cmsplugin_cascade.link.forms import LinkForm from .conftest import UserFactory @@ -82,7 +83,7 @@ def test_iconfont_change_view(admin_client, icon_font): @pytest.mark.django_db def simple_icon(admin_site, cms_placeholder, icon_font): """Create and edit a SimpleIconPlugin""" - class IconFontForm(IconFormMixin, ModelForm): + class IconFontForm(IconFormMixin, LinkForm, ModelForm): class Meta(IconFormMixin.Meta): model = CascadeElement @@ -91,7 +92,8 @@ class Meta(IconFormMixin.Meta): assert isinstance(simple_icon_model, CascadeElement) # edit simple icon plugin - data = {'icon_font': str(icon_font.id), 'symbol': 'icon-skiing'} + data = {'icon_font': str(icon_font.id), 'symbol': 'icon-skiing', + 'link_type': 'exturl', 'ext_url': 'http://test.ru/test', 'link_target': '_blank', 'link_title': 'test title'} form = IconFontForm(data=data, instance=simple_icon_model) assert form.is_valid() simple_icon_model = form.save() @@ -110,4 +112,4 @@ def test_simple_icon(rf, simple_icon): context = RequestContext(request) content_renderer = ContentRenderer(request) html = content_renderer.render_plugin(simple_icon_model, context).strip() - assert html == '' + assert html == '' diff --git a/tests/test_segmentation.py b/tests/test_segmentation.py index 6343a1fec..9c5083bd1 100644 --- a/tests/test_segmentation.py +++ b/tests/test_segmentation.py @@ -19,14 +19,8 @@ class SegmentationPluginTest(CascadeTestCase): def setUp(self): super(SegmentationPluginTest, self).setUp() UserModel = get_user_model() - try: - self.staff_user = UserModel.objects.get(username='staff') - except UserModel.DoesNotExist: - self.staff_user = self.get_staff_user_with_no_permissions() - try: - self.staff_user = UserModel.objects.get(username='staff') - except UserModel.DoesNotExist: - self.staff_user = self.get_staff_user_with_no_permissions() + self.admin_user = UserModel.objects.get(username='admin') + self.staff_user = UserModel.objects.get(username='staff') def test_plugin_context(self): # create container @@ -64,16 +58,17 @@ def test_plugin_context(self): text_model_staff, else_segment_model, text_model_anon] build_plugin_tree(plugin_list) - # render the plugins as admin user + # test for if-segment (render the plugins as admin user) + self.request.user = self.admin_user soup = BeautifulSoup(self.get_html(wrapper_model, self.get_request_context()), 'html.parser') self.assertHTMLEqual(soup.p.text, 'User is admin') - # render the plugins as staff user + # test for elif-segment (render the plugins as staff user) self.request.user = self.staff_user soup = BeautifulSoup(self.get_html(wrapper_model, self.get_request_context()), 'html.parser') self.assertHTMLEqual(soup.p.text, 'User is staff') - # render the plugins as anonymous user + # test for else-segment (render the plugins as anonymous user) self.request.user = AnonymousUser soup = BeautifulSoup(self.get_html(wrapper_model, self.get_request_context()), 'html.parser') self.assertHTMLEqual(soup.p.text, 'User is anonymous') diff --git a/tests/test_strides.py b/tests/test_strides.py index b0d86291b..982f26926 100644 --- a/tests/test_strides.py +++ b/tests/test_strides.py @@ -21,6 +21,7 @@ class StridePluginTest(CascadeTestCase): def setUp(self): + super().setUp() request = RequestFactory().get('/') self.context = RequestContext(request, {}) diff --git a/tests/test_text_link_plugin.py b/tests/test_text_link_plugin.py new file mode 100644 index 000000000..cbdd4556b --- /dev/null +++ b/tests/test_text_link_plugin.py @@ -0,0 +1,41 @@ +import pytest +from django.forms.models import ModelForm +from django.template.context import RequestContext +from cms.api import add_plugin +from cms.plugin_rendering import ContentRenderer +from cmsplugin_cascade.models import CascadeElement +from cmsplugin_cascade.link.forms import LinkForm +from cmsplugin_cascade.link.cms_plugins import TextLinkPlugin + + +@pytest.fixture +@pytest.mark.django_db +def link(admin_site, cms_placeholder): + """Create and edit a TextLinkPlugin""" + class LinkModelForm(LinkForm, ModelForm): + class Meta(LinkForm.Meta): + model = CascadeElement + + # add text link plugin + link_model = add_plugin(cms_placeholder, TextLinkPlugin, 'en') + assert isinstance(link_model, CascadeElement) + + # edit text link plugin + data = {'link_type': 'exturl', 'ext_url': 'http://test.ru/test', 'link_target': '_blank', 'link_title': 'test title'} + form = LinkModelForm(data=data, instance=link_model) + assert form.is_valid() + link_model = form.save() + link_plugin = link_model.get_plugin_class_instance(admin_site) + assert isinstance(link_plugin, TextLinkPlugin) + return link_plugin, link_model + + +@pytest.mark.django_db +def test_link_plugin(rf, link): + """Render a LinkPluginBase""" + link_plugin, link_model = link + request = rf.get('/') + context = RequestContext(request) + content_renderer = ContentRenderer(request) + html = content_renderer.render_plugin(link_model, context).strip() + assert html == '' diff --git a/tox.ini b/tox.ini index 30a925f56..f88d5e6b5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] downloadcache = {toxworkdir}/_download/ -envlist = coverage-clean, py{35,36,37}-django{21,22}-cms{36,37}, coverage-report +envlist = coverage-clean, py{35,36,37,38}-django{21,22,30}-cms{36,37}, coverage-report [testenv] # usedevelop is needed to collect coverage data @@ -14,7 +14,6 @@ deps = -r requirements/base.txt -r tests/requirements.txt Django-Select2 - djangocms-helper==1.2.0 coverage cms36: django-cms<3.7 cms37: django-cms<3.8