diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b7f33f..ac32ef9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,10 +16,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.8, 3.9, '3.10' ] + python-version: [ 3.9, '3.10', '3.11' ] requirements-file: [ - dj32_cms40.txt, - dj42_cms40.txt, + dj42_cms41.txt, ] steps: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b51a2d7..ea1b6ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,12 @@ Changelog Unreleased ========== +* Dropped Support for Python 3.8 +* Dropped Support for Django CMS < 4.1 +* Introduced Django CMS 4.1 support. +* delete `VersionLock` as It's moved to Version.locked_by field. +* monkeypatch `ChangeListActionsMixin` in cms.admin.utils to support action list burger menu. + 1.3.0 (2024-05-16) ================== diff --git a/djangocms_version_locking/admin.py b/djangocms_version_locking/admin.py deleted file mode 100644 index 2628ae9..0000000 --- a/djangocms_version_locking/admin.py +++ /dev/null @@ -1,25 +0,0 @@ -from djangocms_versioning.admin import VersioningAdminMixin - - -class VersionLockAdminMixin(VersioningAdminMixin): - """ - Mixin providing versioning functionality to admin classes of - version models. - """ - - def has_change_permission(self, request, obj=None): - """ - If there’s a lock for edited object and if that lock belongs - to the current user - """ - from .helpers import content_is_unlocked_for_user - - # User has permissions? - has_permission = super().has_change_permission(request, obj) - if not has_permission: - return False - - # Check if the lock exists and belongs to the user - if obj: - return content_is_unlocked_for_user(obj, request.user) - return True diff --git a/djangocms_version_locking/apps.py b/djangocms_version_locking/apps.py index 41192ae..3aea8d4 100644 --- a/djangocms_version_locking/apps.py +++ b/djangocms_version_locking/apps.py @@ -7,4 +7,4 @@ class VersionLockingConfig(AppConfig): verbose_name = _("django CMS Version Locking") def ready(self): - from .monkeypatch import checks, cms_toolbars, models # noqa: F401 + from .monkeypatch import cms_toolbars # noqa: F401 diff --git a/djangocms_version_locking/cms_config.py b/djangocms_version_locking/cms_config.py index 63080ef..5e95235 100644 --- a/djangocms_version_locking/cms_config.py +++ b/djangocms_version_locking/cms_config.py @@ -1,23 +1,24 @@ -from django.template.loader import render_to_string from django.utils.html import format_html +from django.utils.safestring import mark_safe from cms.app_base import CMSAppConfig, CMSAppExtension from djangocms_alias.models import AliasContent from djangocms_versioning.constants import DRAFT - -from djangocms_version_locking.helpers import version_is_locked +from djangocms_versioning.helpers import version_is_locked def add_alias_version_lock(obj, field): - version = obj.versions.all()[0] + # add None obj check, if the legacy data has empty value. lock_icon = "" - if version.state == DRAFT and version_is_locked(version): - lock_icon = render_to_string("djangocms_version_locking/admin/locked_mixin_icon.html") + if obj: + version = obj.versions.all()[0] + if version.state == DRAFT and version_is_locked(version): + lock_icon = mark_safe('') return format_html( "{is_locked}{field_value}", is_locked=lock_icon, - field_value=getattr(obj, field), + field_value=getattr(obj, field, '-'), ) @@ -27,7 +28,12 @@ def __init__(self): # is registered and can be overriden without requiring a strict load order # in the INSTALLED_APPS setting in a projects settings.py. This is why this patch # Isn't loaded from: VersionLockingConfig.ready - from .monkeypatch import admin as monkeypatch_admin # noqa: F401 + from .monkeypatch import ( # noqa: F401 + admin as monkeypatched_version_admin, + ) + from .monkeypatch.djangocms_alias import ( # noqa: F401 + admin as monkeypatched_alias_admin, + ) def configure_app(self, cms_config): pass diff --git a/djangocms_version_locking/conf.py b/djangocms_version_locking/conf.py deleted file mode 100644 index 5787aa6..0000000 --- a/djangocms_version_locking/conf.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf import settings - - -EMAIL_NOTIFICATIONS_FAIL_SILENTLY = getattr( - settings, "EMAIL_NOTIFICATIONS_FAIL_SILENTLY", False -) diff --git a/djangocms_version_locking/emails.py b/djangocms_version_locking/emails.py deleted file mode 100644 index 00bf60a..0000000 --- a/djangocms_version_locking/emails.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from cms.toolbar.utils import get_object_preview_url -from cms.utils import get_current_site - -from .helpers import send_email -from .utils import get_absolute_url - - -def notify_version_author_version_unlocked(version, unlocking_user): - # If the unlocking user is the current author, don't send a notification email - if version.created_by == unlocking_user: - return - - # If the users name is available use it, otherwise use their username - username = unlocking_user.get_full_name() or unlocking_user.username - - site = get_current_site() - recipients = [version.created_by.email] - subject = "[Django CMS] ({site_name}) {title} - {description}".format( - site_name=site.name, - title=version.content, - description=_("Unlocked"), - ) - version_url = get_absolute_url( - get_object_preview_url(version.content) - ) - - # Prepare and send the email - template_context = { - 'version_link': version_url, - 'by_user': username, - } - status = send_email( - recipients=recipients, - subject=subject, - template='unlock-notification.txt', - template_context=template_context, - ) - return status diff --git a/djangocms_version_locking/helpers.py b/djangocms_version_locking/helpers.py deleted file mode 100644 index 1f405b5..0000000 --- a/djangocms_version_locking/helpers.py +++ /dev/null @@ -1,168 +0,0 @@ -from django.conf import settings -from django.contrib import admin -from django.core.exceptions import ObjectDoesNotExist -from django.core.mail import EmailMessage -from django.template.loader import render_to_string -from django.utils.encoding import force_str - -from djangocms_versioning import versionables -from djangocms_versioning.models import Version - -from .admin import VersionLockAdminMixin -from .conf import EMAIL_NOTIFICATIONS_FAIL_SILENTLY -from .models import VersionLock - - -try: - from djangocms_internalsearch.helpers import emit_content_change -except ImportError: - emit_content_change = None - - -def version_lock_admin_factory(admin_class): - """A class factory returning admin class with overriden - versioning functionality. - - :param admin_class: Existing admin class - :return: A subclass of `VersionLockAdminMixin` and `admin_class` - """ - return type('VersionLocking' + admin_class.__name__, (VersionLockAdminMixin, admin_class), {}) - - -def _replace_admin_for_model(modeladmin, admin_site): - """Replaces existing admin class registered for `modeladmin.model` with - a subclass that includes version locking functionality. - - Doesn't do anything if `modeladmin` is already an instance of - `VersionLockAdminMixin`. - - :param model: ModelAdmin instance - :param admin_site: AdminSite instance - """ - if isinstance(modeladmin, VersionLockAdminMixin): - return - new_admin_class = version_lock_admin_factory(modeladmin.__class__) - admin_site.unregister(modeladmin.model) - admin_site.register(modeladmin.model, new_admin_class) - - -def replace_admin_for_models(models, admin_site=None): - """ - :param models: List of model classes - :param admin_site: AdminSite instance - """ - if admin_site is None: - admin_site = admin.site - for model in models: - try: - modeladmin = admin_site._registry[model] - except KeyError: - continue - _replace_admin_for_model(modeladmin, admin_site) - - -def get_lock_for_content(content): - """Check if a lock exists, if so return it - """ - try: - versionables.for_content(content) - except KeyError: - return None - try: - version = Version.objects.select_related('versionlock').get_for_content(content) - return version.versionlock - except ObjectDoesNotExist: - return None - - -def content_is_unlocked_for_user(content, user): - """Check if lock doesn't exist or object is locked to provided user. - """ - lock = get_lock_for_content(content) - if lock is None: - return True - return lock.created_by == user - - -def placeholder_content_is_unlocked_for_user(placeholder, user): - """Check if lock doesn't exist or placeholder source object - is locked to provided user. - """ - content = placeholder.source - return content_is_unlocked_for_user(content, user) - - -def create_version_lock(version, user): - """ - Create a version lock if necessary - """ - lock, created = VersionLock.objects.get_or_create( - version=version, - created_by=user - ) - if created and emit_content_change: - emit_content_change(version.content) - return lock - - -def remove_version_lock(version): - """ - Delete a version lock, handles when there are none available. - """ - deleted = VersionLock.objects.filter(version=version).delete() - if deleted[0] and emit_content_change: - emit_content_change(version.content) - return deleted - - -def version_is_locked(version): - """ - Determine if a version is locked - """ - return getattr(version, "versionlock", None) - - -def version_is_unlocked_for_user(version, user): - """Check if lock doesn't exist for a version object or is locked to provided user. - """ - lock = version_is_locked(version) - return lock is None or lock.created_by == user - - -def send_email( - recipients, - subject, - template, - template_context -): - """ - Send emails using locking templates - """ - template = 'djangocms_version_locking/emails/{}'.format(template) - subject = force_str(subject) - content = render_to_string(template, template_context) - - message = EmailMessage( - subject=subject, - body=content, - from_email=settings.DEFAULT_FROM_EMAIL, - to=recipients, - ) - return message.send( - fail_silently=EMAIL_NOTIFICATIONS_FAIL_SILENTLY - ) - - -def get_latest_draft_version(version): - """Get latest draft version of version object - """ - from djangocms_versioning.constants import DRAFT - from djangocms_versioning.models import Version - - drafts = ( - Version.objects - .filter_by_content_grouping_values(version.content) - .filter(state=DRAFT) - ) - - return drafts.first() diff --git a/djangocms_version_locking/migrations/0002_migrate_locked_by.py b/djangocms_version_locking/migrations/0002_migrate_locked_by.py new file mode 100644 index 0000000..f9f2502 --- /dev/null +++ b/djangocms_version_locking/migrations/0002_migrate_locked_by.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.8 on 2024-11-07 07:21 + +from django.db import migrations + + +def forwards(apps, schema_editor): + db_alias = schema_editor.connection.alias + Version = apps.get_model("djangocms_versioning", "Version") + VersionLock = apps.get_model("djangocms_version_locking", "VersionLock") + + version_lock_qs = VersionLock.objects.using(db_alias).all() + versions = set(Version.objects.using(db_alias).filter(pk__in=version_lock_qs.values('version'))) + + for version in versions: + version.locked_by = version.versionlock.created_by + Version.objects.using(db_alias).bulk_update(versions, ['locked_by']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_version_locking', '0001_initial'), + ('djangocms_versioning', '0017_merge_20230514_1027'), + ] + + operations = [ + migrations.RunPython(forwards, reverse_code=migrations.RunPython.noop) + ] diff --git a/djangocms_version_locking/migrations/0003_delete_versionlock.py b/djangocms_version_locking/migrations/0003_delete_versionlock.py new file mode 100644 index 0000000..9c3046f --- /dev/null +++ b/djangocms_version_locking/migrations/0003_delete_versionlock.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.16 on 2024-11-07 14:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_version_locking', '0002_migrate_locked_by'), + ] + + operations = [ + migrations.DeleteModel( + name='VersionLock', + ), + ] diff --git a/djangocms_version_locking/models.py b/djangocms_version_locking/models.py index 99a1fd2..7f6d590 100644 --- a/djangocms_version_locking/models.py +++ b/djangocms_version_locking/models.py @@ -1,19 +1,22 @@ -from django.conf import settings -from django.db import models -from django.utils.translation import gettext_lazy as _ +# NOTE: 'version lock' has been migrated to djangocms_versioning.Version table 'locked_by' field. +# so this table is not needed anymore after data migration. -from djangocms_versioning.models import Version +# from django.conf import settings +# from django.db import models +# from django.utils.translation import gettext_lazy as _ +# from djangocms_versioning.models import Version -class VersionLock(models.Model): - created = models.DateTimeField(auto_now_add=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.PROTECT, - verbose_name=_('locked by') - ) - version = models.OneToOneField( - Version, - on_delete=models.CASCADE, - verbose_name=_('version') - ) + +# class VersionLock(models.Model): +# created = models.DateTimeField(auto_now_add=True) +# created_by = models.ForeignKey( +# settings.AUTH_USER_MODEL, +# on_delete=models.PROTECT, +# verbose_name=_('locked by') +# ) +# version = models.OneToOneField( +# Version, +# on_delete=models.CASCADE, +# verbose_name=_('version') +# ) diff --git a/djangocms_version_locking/monkeypatch/admin.py b/djangocms_version_locking/monkeypatch/admin.py index 9600b6a..2a8e276 100644 --- a/djangocms_version_locking/monkeypatch/admin.py +++ b/djangocms_version_locking/monkeypatch/admin.py @@ -1,172 +1,80 @@ -from django.contrib import messages -from django.contrib.admin.utils import unquote -from django.http import Http404, HttpResponseForbidden, HttpResponseNotAllowed -from django.shortcuts import redirect from django.template.loader import render_to_string -from django.urls import re_path, reverse -from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ -from djangocms_versioning import admin, constants -from djangocms_versioning.helpers import version_list_url +from cms.admin.utils import ChangeListActionsMixin +from cms.utils.urlutils import static_with_version -from djangocms_version_locking.emails import ( - notify_version_author_version_unlocked, +from djangocms_versioning.admin import ( + ExtendedVersionAdminMixin, + StateIndicatorMixin, ) -from djangocms_version_locking.helpers import ( - remove_version_lock, - version_is_locked, +from djangocms_versioning.constants import INDICATOR_DESCRIPTIONS +from djangocms_versioning.helpers import ( + get_latest_admin_viewable_content, + version_list_url, ) +from djangocms_versioning.indicators import content_indicator -def locked(self, version): - """ - Generate an locked field for Versioning Admin - """ - if version.state == constants.DRAFT and version_is_locked(version): - return render_to_string('djangocms_version_locking/admin/locked_icon.html') - return "" - - -locked.short_description = _('locked') -admin.VersionAdmin.locked = locked - - -def get_list_display(func): - """ - Register the locked field with the Versioning Admin - """ +def _get_indicator_column(func): + ''' + Change the State Indicator to readonly, publish process will be take over by djangocms-moderation. + ''' def inner(self, request): - list_display = func(self, request) - created_by_index = list_display.index('created_by') - return list_display[:created_by_index] + ('locked', ) + list_display[created_by_index:] - return inner - - -admin.VersionAdmin.get_list_display = get_list_display(admin.VersionAdmin.get_list_display) - - -def _unlock_view(self, request, object_id): - """ - Unlock a locked version - """ - # This view always changes data so only POST requests should work - if request.method != 'POST': - return HttpResponseNotAllowed(['POST'], _('This view only supports POST method.')) - - # Check version exists - version = self.get_object(request, unquote(object_id)) - if version is None: - return self._get_obj_does_not_exist_redirect( - request, self.model._meta, object_id) - - # Raise 404 if not locked - if version.state != constants.DRAFT: - raise Http404 - - # Check that the user has unlock permission - if not request.user.has_perm('djangocms_version_locking.delete_versionlock'): - return HttpResponseForbidden(force_str(_("You do not have permission to remove the version lock"))) - - # Unlock the version - remove_version_lock(version) - # Display message - messages.success(request, _("Version unlocked")) - - # Send an email notification - notify_version_author_version_unlocked(version, request.user) - - # Redirect - url = version_list_url(version.content) - return redirect(url) - - -admin.VersionAdmin._unlock_view = _unlock_view - - -def _get_unlock_link(self, obj, request): - """ - Generate an unlock link for the Versioning Admin - """ - # If the version is not draft no action should be present - if obj.state != constants.DRAFT or not version_is_locked(obj): - return "" - - disabled = True - # Check whether the lock can be removed - # Check that the user has unlock permission - if version_is_locked(obj) and request.user.has_perm('djangocms_version_locking.delete_versionlock'): - disabled = False - - unlock_url = reverse('admin:{app}_{model}_unlock'.format( - app=obj._meta.app_label, model=self.model._meta.model_name, - ), args=(obj.pk,)) - - return render_to_string( - 'djangocms_version_locking/admin/unlock_icon.html', - { - 'unlock_url': unlock_url, - 'disabled': disabled - } - ) - - -admin.VersionAdmin._get_unlock_link = _get_unlock_link - - -def _get_urls(func): - """ - Add custom Version Lock urls to Versioning urls - """ - def inner(self, *args, **kwargs): - url_list = func(self, *args, **kwargs) - info = self.model._meta.app_label, self.model._meta.model_name - url_list.insert(0, re_path( - r'^(.+)/unlock/$', - self.admin_site.admin_view(self._unlock_view), - name='{}_{}_unlock'.format(*info), - )) - return url_list + def indicator(obj): + if self._extra_grouping_fields is not None: # Grouper Model + content_obj = get_latest_admin_viewable_content(obj, include_unpublished_archived=True, **{ + field: getattr(self, field) for field in self._extra_grouping_fields + }) + else: # Content Model + content_obj = obj + status = content_indicator(content_obj) + return render_to_string( + "admin/djangocms_versioning/indicator.html", + { + "state": status or "empty", + "description": INDICATOR_DESCRIPTIONS.get(status, _("Empty")), + "menu_template": "admin/cms/page/tree/indicator_menu.html", + } + ) + indicator.short_description = self.indicator_column_label + return indicator return inner -admin.VersionAdmin.get_urls = _get_urls(admin.VersionAdmin.get_urls) - - -def get_state_actions(func): - """ - Add custom Version Lock actions to Versioning state actions - """ - def inner(self, *args, **kwargs): - state_list = func(self, *args, **kwargs) - state_list.append(self._get_unlock_link) - return state_list +def _get_actions_list(func): + ''' + Add `Manage versions` action to versioned admin's action list, State Indicator is ready only. + ''' + def inner(self): + actions = func(self) + if self._get_manage_versions_link not in actions: + actions.append(self._get_manage_versions_link) + return actions return inner -admin.VersionAdmin.get_state_actions = get_state_actions(admin.VersionAdmin.get_state_actions) - - -def _get_edit_redirect_version(func): - """ - Override the Versioning Admin edit redirect to add a user as a version author if no lock exists - """ - def inner(self, request, object_id): - version = func(self, request, object_id) - if version is not None: - # Add the current user as the version author - # Saving the version will Add a lock to the current user editing now it's in an unlocked state - version.created_by = request.user - version.save() - return version - return inner +def _get_manage_versions_link(self, obj, request, disabled=False): + url = version_list_url(obj) + return self.admin_action_button( + url, + icon="list-ol", + title=_("Manage versions"), + name="manage-versions", + disabled=disabled, + ) -admin.VersionAdmin._get_edit_redirect_version = _get_edit_redirect_version( - admin.VersionAdmin._get_edit_redirect_version +""" Monkeypatch ChangeListActionsMixin, add action burger menu for action list +This will enable burger menu feature for ModelAdmin inherits from +VersionAdmin, ExtendedVersionAdminMixin, GrouperModelAdmin. +""" +ChangeListActionsMixin.Media.css["all"] += ( + static_with_version("cms/css/cms.pagetree.css"), + "djangocms_version_locking/css/actions.css", ) +ChangeListActionsMixin.Media.js += ("djangocms_version_locking/js/actions.js",) - -# Add Version Locking css media to the Versioning Admin instance -additional_css = ('djangocms_version_locking/css/version-locking.css',) -admin.VersionAdmin.Media.css['all'] = admin.VersionAdmin.Media.css['all'] + additional_css +ExtendedVersionAdminMixin._get_manage_versions_link = _get_manage_versions_link +ExtendedVersionAdminMixin.get_actions_list = _get_actions_list(ExtendedVersionAdminMixin.get_actions_list) +StateIndicatorMixin.get_indicator_column = _get_indicator_column(StateIndicatorMixin.get_indicator_column) diff --git a/djangocms_version_locking/monkeypatch/checks.py b/djangocms_version_locking/monkeypatch/checks.py deleted file mode 100644 index 509ed58..0000000 --- a/djangocms_version_locking/monkeypatch/checks.py +++ /dev/null @@ -1,8 +0,0 @@ -from cms.models import fields - -from djangocms_version_locking.helpers import ( - placeholder_content_is_unlocked_for_user, -) - - -fields.PlaceholderRelationField.default_checks += [placeholder_content_is_unlocked_for_user] diff --git a/djangocms_version_locking/monkeypatch/cms_toolbars.py b/djangocms_version_locking/monkeypatch/cms_toolbars.py index 57584ea..63b4a07 100644 --- a/djangocms_version_locking/monkeypatch/cms_toolbars.py +++ b/djangocms_version_locking/monkeypatch/cms_toolbars.py @@ -4,11 +4,8 @@ from cms.toolbar.items import Button, ButtonList from djangocms_versioning.cms_toolbars import VersioningToolbar - -from djangocms_version_locking.helpers import ( - content_is_unlocked_for_user, - get_lock_for_content, -) +from djangocms_versioning.helpers import content_is_unlocked_for_user +from djangocms_versioning.models import Version class ButtonWithAttributes(Button): @@ -40,11 +37,11 @@ def inner(self, **kwargs): # Populate a title with the locked author details html_attributes = {} - version_lock = get_lock_for_content(self.toolbar.obj) + version_lock = Version.objects.get_for_content(self.toolbar.obj).locked_by if version_lock: # If the users name is available use it, otherwise use their username html_attributes['title'] = _("Locked with {name}").format( - name=version_lock.created_by.get_full_name() or version_lock.created_by.username, + name=version_lock.get_full_name() or version_lock.username, ) # There is a version lock for the current object. diff --git a/djangocms_version_locking/monkeypatch/djangocms_alias/__init__.py b/djangocms_version_locking/monkeypatch/djangocms_alias/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangocms_version_locking/monkeypatch/djangocms_alias/admin.py b/djangocms_version_locking/monkeypatch/djangocms_alias/admin.py new file mode 100644 index 0000000..5e54855 --- /dev/null +++ b/djangocms_version_locking/monkeypatch/djangocms_alias/admin.py @@ -0,0 +1,97 @@ +from django.contrib import admin +from django.http import HttpRequest +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from djangocms_alias.admin import AliasAdmin as OriginalAliasAdmin +from djangocms_alias.models import Alias +from djangocms_versioning.constants import DRAFT, PUBLISHED +from djangocms_versioning.helpers import ( + get_latest_admin_viewable_content, + proxy_model, + version_list_url, +) + +from djangocms_version_locking.utils import get_registered_admin + + +class AliasAdmin(get_registered_admin(Alias, OriginalAliasAdmin)): + + change_list_template = "monkeypatch/cms/admin/cms/grouper/change_list.html" + + def get_actions_list(self) -> list: + """Add alias manage version link""" + original_list = super().get_actions_list() + settings_link_index = original_list.index(self._get_settings_action) + preview_link_index = original_list.index(self._get_view_action) + original_list.insert(settings_link_index, self._get_manage_versions_link) + original_list.insert(preview_link_index+1, self._get_edit_link) + return original_list + + def _get_content_obj(self, obj: Alias): + if self._extra_grouping_fields is not None: # Grouper Model + content_obj = get_latest_admin_viewable_content(obj, include_unpublished_archived=True, **{ + field: getattr(self, field) for field in self._extra_grouping_fields + }) + else: # Content Model + content_obj = obj + return content_obj + + def _get_edit_link(self, obj: Alias, request: HttpRequest, disabled: bool = False): + obj_content = self._get_content_obj(obj) + if not obj_content: + # Don't display the link if it can't be edited, as the content is empty. + return "" + version = proxy_model(obj_content.versions.all()[0], obj_content) + + if version.state not in (DRAFT, PUBLISHED): + # Don't display the link if it can't be edited + return "" + if not version.check_edit_redirect.as_bool(request.user): + disabled = True + + if version.state == PUBLISHED: + icon = "edit-new" + title = "New Draft" + else: + icon = "pencil" + title = "Edit" + + url = reverse( + "admin:{app}_{model}_edit_redirect".format( + app=version._meta.app_label, model=version._meta.model_name + ), + args=(version.pk,), + ) + + # close sideframe as edit will always be on page and not in sideframe + return self.admin_action_button( + url=url, + icon=icon, + title=_(title), + name="edit", + disabled=disabled, + action="post", + keepsideframe=False, + ) + + def _get_manage_versions_link(self, obj: Alias, request: HttpRequest, disabled: bool = False): + obj_content = self._get_content_obj(obj) + if not obj_content: + # Don't display the link if it can't find version list urls, as the content is empty. + return "" + url = version_list_url(obj_content) + return self.admin_action_button( + url, + icon="list-ol", + title=_("Manage versions"), + name="manage-versions", + disabled=disabled, + ) + + def has_delete_permission(self, request: HttpRequest, obj=None): + return False + + +admin.site.unregister(Alias) +admin.site.register(Alias, AliasAdmin) diff --git a/djangocms_version_locking/monkeypatch/models.py b/djangocms_version_locking/monkeypatch/models.py deleted file mode 100644 index 74fe33b..0000000 --- a/djangocms_version_locking/monkeypatch/models.py +++ /dev/null @@ -1,102 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from djangocms_moderation import models as moderation_model -from djangocms_moderation.helpers import ( - get_moderated_children_from_placeholder, -) -from djangocms_versioning import constants, models -from djangocms_versioning.exceptions import ConditionFailed - -from djangocms_version_locking.helpers import ( - create_version_lock, - get_latest_draft_version, - remove_version_lock, - version_is_locked, -) - - -def new_save(old_save): - """ - Override the Versioning save method to add a version lock - """ - def inner(version, **kwargs): - old_save(version, **kwargs) - # A draft version is locked by default - if version.state == constants.DRAFT: - if not version_is_locked(version): - # create a lock - create_version_lock(version, version.created_by) - # A any other state than draft has no lock, an existing lock should be removed - else: - remove_version_lock(version) - return version - return inner - - -models.Version.save = new_save(models.Version.save) - - -def _is_version_locked(message): - def inner(version, user): - lock = version_is_locked(version) - if lock and lock.created_by != user: - raise ConditionFailed(message.format(user=lock.created_by)) - return inner - - -def _is_draft_version_locked(message): - def inner(version, user): - try: - # if there's a prepoluated field on version object - # representing a draft lock, use it - cached_draft_version_user_id = getattr(version, "_draft_version_user_id") - if cached_draft_version_user_id and cached_draft_version_user_id != user.pk: - raise ConditionFailed( - message.format(user="User #{}".format(cached_draft_version_user_id)) - ) - except AttributeError: - draft_version = get_latest_draft_version(version) - lock = version_is_locked(draft_version) - if lock and lock.created_by != user: - raise ConditionFailed(message.format(user=lock.created_by)) - return inner - - -error_message = _('Action Denied. The latest version is locked with {user}') -draft_error_message = _('Action Denied. The draft version is locked with {user}') - - -models.Version.check_archive += [_is_version_locked(error_message)] -models.Version.check_discard += [_is_version_locked(error_message)] -models.Version.check_revert += [_is_draft_version_locked(draft_error_message)] -models.Version.check_unpublish += [_is_draft_version_locked(draft_error_message)] -models.Version.check_edit_redirect += [_is_draft_version_locked(draft_error_message)] - - -def _add_nested_children(self, version, parent_node): - """ - Helper method which finds moderated children and adds them to the collection if - it's not locked by a user - """ - parent = version.content - added_items = 0 - if not getattr(parent, "get_placeholders", None): - return added_items - - for placeholder in parent.get_placeholders(): - for child_version in get_moderated_children_from_placeholder( - placeholder, version.versionable.grouping_values(parent) - ): - # Don't add the version if it's locked by another user - child_version_locked = version_is_locked(child_version) - if not child_version_locked: - moderation_request, _added_items = self.add_version( - child_version, parent=parent_node, include_children=True - ) - else: - _added_items = self._add_nested_children(child_version, parent_node) - added_items += _added_items - return added_items - - -moderation_model.ModerationCollection._add_nested_children = _add_nested_children diff --git a/djangocms_version_locking/static/djangocms_version_locking/css/actions.css b/djangocms_version_locking/static/djangocms_version_locking/css/actions.css new file mode 100644 index 0000000..fc4ae2f --- /dev/null +++ b/djangocms_version_locking/static/djangocms_version_locking/css/actions.css @@ -0,0 +1,108 @@ +/*------------------------------------- + General versioining action list styles +---------------------------------------*/ + +/* ensure certain columns aren't too wide */ +.field-get_title { + min-width: 250px; + word-break: break-word; + white-space: normal !important; +} + +.field-url { + min-width: 100px; + word-break: break-all; + white-space: normal !important; +} +.cms-icon-menu { + cursor: pointer; +} + +/*------------------------------------- + All action buttons +---------------------------------------*/ +.btn.cms-action-btn { + position: relative; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + padding: 0 4px 0 7px !important; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + height: 34px; + margin-top: -12px !important; + position: relative; + height: 34px; + bottom: -6px; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +/* disable clicking for inactive buttons */ +.btn.cms-action-btn.inactive { + pointer-events: none; + background-color: #e1e1e1 !important; +} + +.btn.cms-action-btn.inactive span { + opacity: 0.5; +} + +/*------------------------------------- +This governs the drop-down behaviour +extending the pagetree classes provided by CMS +---------------------------------------*/ + +/* add shadow on burger menu trigger */ +a.btn.cms-action-btn:hover, a.btn.cms-action-btn.open { + box-shadow: inset 0 3px 5px rgba(0,0,0,.125); +} + +/* style for each option row */ +ul.cms-pagetree-dropdown-menu-inner li { + border: 1px solid transparent; + border-radius: 5px; + padding: 2px 6px; +} +ul.cms-pagetree-dropdown-menu-inner li:hover { + list-style-type: none; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #0bf; +} + +/* align the option text with it's icon */ +ul.cms-pagetree-dropdown-menu-inner li a span { + line-height: 1rem; +} + +/* disable any inactive option */ +ul.cms-pagetree-dropdown-menu-inner li a.inactive { + cursor: not-allowed; + pointer-events: none; + opacity: 0.3; + filter: alpha(opacity=30); +} + +/* set the size of the drop-down */ +.cms-pagetree-dropdown-menu.open { + display: block; + width: 200px; +} + +/* hide when closed */ +.cms-pagetree-dropdown-menu.closed, .cms-icon-menu.closed .cms-pagetree-dropdown-menu { + display: none; +} + +.cms-pagetree-dropdown-menu:before { + margin-top: 13px; +} + +.btn.cms-action-btn.cms-action-burger { + margin-right: 4px !important; +} diff --git a/djangocms_version_locking/static/djangocms_version_locking/js/actions.js b/djangocms_version_locking/static/djangocms_version_locking/js/actions.js new file mode 100644 index 0000000..e4a980e --- /dev/null +++ b/djangocms_version_locking/static/djangocms_version_locking/js/actions.js @@ -0,0 +1,183 @@ +"use strict"; + +(function ($) { + if (!$) { + return; + } + + $(function () { + var createBurgerMenu = function createBurgerMenu(row) { + /* create burger menu anchor section */ + var anchor = document.createElement('A'); + var cssclass = document.createAttribute('class'); + cssclass.value = 'btn cms-action-btn closed cms-action-burger'; + anchor.setAttributeNode(cssclass); + // create burger menu title + var title = document.createAttribute('title'); + title.value = 'Actions'; + anchor.setAttributeNode(title); + // create burger menu icon + var menu_icon = document.createElement('span'); + menu_icon.className = "cms-icon cms-icon-menu"; + anchor.appendChild(menu_icon); + + /* create options container */ + var optionsContainer = document.createElement('DIV'); + cssclass = document.createAttribute('class'); + cssclass.value = 'cms-pagetree-dropdown-menu ' + // main selector for the menu + 'cms-pagetree-dropdown-menu-arrow-right-top'; // keeps the menu arrow in position + + optionsContainer.setAttributeNode(cssclass); + var ul = document.createElement('UL'); + cssclass = document.createAttribute('class'); + cssclass.value = 'cms-pagetree-dropdown-menu-inner'; + ul.setAttributeNode(cssclass); + /* get the existing actions and move them into the options container */ + + var li; + var text; + var actions = $(row).children('.field-list_actions'); + + if (!actions.length) { + /* skip any rows without actions to avoid errors */ + return; + } + + var actions_btn = $(actions[0]).children('.cms-action-btn'); + if (actions_btn.length <=3) { + return; + } else { + actions_btn.each(function (index, item) { + /* exclude preview/view and edit buttons */ + if (item.classList.contains('cms-action-preview') || + item.classList.contains('cms-action-view') || + item.classList.contains('cms-action-edit')) { + return; + } + + li = document.createElement('LI'); + /* create an anchor from the item */ + + var li_anchor = document.createElement('A'); + cssclass = document.createAttribute('class'); + cssclass.value = 'cms-action-burger-options-anchor'; + + if ($(item).hasClass('cms-form-get-method')) { + /* ensure the fake-form selector is propagated to the new anchor */ + cssclass.value += ' cms-form-get-method'; + } + + li_anchor.setAttributeNode(cssclass); + var href = document.createAttribute('href'); + href.value = $(item).attr('href'); + li_anchor.setAttributeNode(href); + /* move the an image element */ + + var existing_icon_span = $(item).children('span, img'); + li_anchor.appendChild(existing_icon_span[0]); + /* create the button text */ + + text = document.createTextNode(item.title); + var span = document.createElement('SPAN'); + span.appendChild(text); // construct the button + + li.appendChild(li_anchor); + li_anchor.appendChild(span); + ul.appendChild(li); + /* destroy original replaced buttons */ + + actions[0].removeChild(item); + }); + } + + /* add the options to the drop-down */ + optionsContainer.appendChild(ul); + var action_children = $(actions[0]).children(); + if (action_children.length > 0 && action_children.last().prop('classList').length < 1) { + $(actions[0]).children().last().before(anchor) + } else { + actions[0].appendChild(anchor) + } + document.body.appendChild(optionsContainer); + + /* listen for burger menu clicks */ + anchor.addEventListener('click', function (ev) { + ev.stopPropagation(); + toggleBurgerMenu(anchor, optionsContainer); + }); + /* close burger menu if clicking outside */ + + $(window).click(function () { + closeBurgerMenu(); + }); + }; + + var toggleBurgerMenu = function toggleBurgerMenu(burgerMenuAnchor, optionsContainer) { + var bm = $(burgerMenuAnchor); + var op = $(optionsContainer); + var closed = bm.hasClass('closed'); + closeBurgerMenu(); + + if (closed) { + bm.removeClass('closed'); + bm.addClass('open'); + op.removeClass('closed'); + op.addClass('open'); + } else { + bm.addClass('closed'); + bm.removeClass('open'); + op.addClass('closed'); + op.removeClass('open'); + } + + var pos = bm.offset(); + op.css('left', pos.left - 200); + op.css('top', pos.top); + }; + + var closeBurgerMenu = function closeBurgerMenu() { + $('.cms-pagetree-dropdown-menu').removeClass('open'); + $('.cms-pagetree-dropdown-menu').addClass('closed'); + $('.cms-action-btn').removeClass('open'); + $('.cms-action-btn').addClass('closed'); + }; + + $('#result_list').find('tr').each(function (index, item) { + createBurgerMenu(item); + }); + /* it is not possible to put a form inside a form, so + actions have to create their own form on click */ + + var fakeForm = function fakeForm(e) { + var action = $(e.currentTarget); + var formMethod = action.attr('class').indexOf('cms-form-get-method') !== -1 ? 'GET' : 'POST'; + if (formMethod == 'GET') return + e.preventDefault(); + + var csrfToken = ''; + var fakeForm = $('
' + csrfToken + '
'); + var keepSideFrame = action.attr('class').indexOf('js-keep-sideframe') !== -1; // always break out of the sideframe, cause it was never meant to open cms views inside it + + try { + if (!keepSideFrame) { + window.top.CMS.API.Sideframe.close(); + } + } catch (err) {} + + if (keepSideFrame) { + var body = window.document.body; + } else { + var body = window.top.document.body; + } + + fakeForm.appendTo(body).submit(); + }; + + $('.js-action, .cms-js-publish-btn, .cms-js-edit-btn, .cms-action-burger-options-anchor').on('click', fakeForm); + $('.js-close-sideframe').on('click', function () { + try { + window.top.CMS.API.Sideframe.close(); + } catch (e) {} + }); + }); +})(typeof django !== 'undefined' && django.jQuery || typeof CMS !== 'undefined' && CMS.$ || false); diff --git a/djangocms_version_locking/templates/djangocms_version_locking/admin/edit_disabled_icon.html b/djangocms_version_locking/templates/djangocms_version_locking/admin/edit_disabled_icon.html deleted file mode 100644 index 29fd80e..0000000 --- a/djangocms_version_locking/templates/djangocms_version_locking/admin/edit_disabled_icon.html +++ /dev/null @@ -1,2 +0,0 @@ -{% load static i18n %} -{% trans "Edit" %} diff --git a/djangocms_version_locking/templates/djangocms_version_locking/admin/unlock_icon.html b/djangocms_version_locking/templates/djangocms_version_locking/admin/unlock_icon.html deleted file mode 100644 index 42542d5..0000000 --- a/djangocms_version_locking/templates/djangocms_version_locking/admin/unlock_icon.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load static i18n %} - - - \ No newline at end of file diff --git a/djangocms_version_locking/templates/djangocms_version_locking/emails/unlock-notification.txt b/djangocms_version_locking/templates/djangocms_version_locking/emails/unlock-notification.txt deleted file mode 100644 index 443c035..0000000 --- a/djangocms_version_locking/templates/djangocms_version_locking/emails/unlock-notification.txt +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n %} -{% blocktrans %} -The following draft version has been unlocked by {{ by_user }} for their use. -{{ version_link }} - -Please note you will not be able to further edit this draft. Kindly reach out to {{ by_user }} in case of any concerns. - -This is an automated notification from Django CMS. -{% endblocktrans %} diff --git a/djangocms_version_locking/templates/monkeypatch/cms/admin/cms/grouper/change_list.html b/djangocms_version_locking/templates/monkeypatch/cms/admin/cms/grouper/change_list.html new file mode 100644 index 0000000..f22148c --- /dev/null +++ b/djangocms_version_locking/templates/monkeypatch/cms/admin/cms/grouper/change_list.html @@ -0,0 +1,11 @@ +{% extends "admin/change_list.html" %}{% load i18n %} +{% block search %} +
+ + +
{{ block.super }} +{% endblock %} diff --git a/djangocms_version_locking/test_utils/test_helpers.py b/djangocms_version_locking/test_utils/test_helpers.py index 22cc006..e5c6e86 100644 --- a/djangocms_version_locking/test_utils/test_helpers.py +++ b/djangocms_version_locking/test_utils/test_helpers.py @@ -53,7 +53,8 @@ def find_toolbar_buttons(button_name, toolbar): """ found = [] for button_list in toolbar.get_right_items(): - found = found + [button for button in button_list.buttons if button.name == button_name] + if hasattr(button_list, "buttons"): + found = found + [button for button in button_list.buttons if button.name == button_name] return found diff --git a/djangocms_version_locking/utils.py b/djangocms_version_locking/utils.py index aa2c224..f89ce98 100644 --- a/djangocms_version_locking/utils.py +++ b/djangocms_version_locking/utils.py @@ -1,18 +1,9 @@ -from __future__ import unicode_literals +from django.contrib import admin -from urllib.parse import urljoin -from django.conf import settings -from django.contrib.sites.models import Site - - -def get_absolute_url(location, site=None): - if not site: - site = Site.objects.get_current() - - if getattr(settings, 'USE_HTTPS', False): - scheme = 'https' +def get_registered_admin(model, defaultAdmin): + admin_instance = admin.site._registry.get(model) + if admin_instance: + return admin_instance.__class__ else: - scheme = 'http' - domain = '{}://{}'.format(scheme, site.domain) - return urljoin(domain, location) + return defaultAdmin diff --git a/docs/notifications.md b/docs/notifications.md index 7325e88..3debd78 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -1,7 +1,2 @@ Notifications ========================== - - -Email notifications ------------------------- -Configure email notifications to fail silently by setting: ``EMAIL_NOTIFICATIONS_FAIL_SILENTLY=True`` diff --git a/setup.py b/setup.py index 6396e95..c602be7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ INSTALL_REQUIREMENTS = [ - 'Django>=3.2,<5.0', + 'Django>=4.0,<5.0', 'django-cms', ] diff --git a/test_settings.py b/test_settings.py index 6ec13a7..c5e190f 100644 --- a/test_settings.py +++ b/test_settings.py @@ -53,6 +53,8 @@ 'PARLER_ENABLE_CACHING': False, 'LANGUAGE_CODE': 'en', 'DEFAULT_AUTO_FIELD': 'django.db.models.AutoField', + 'CMS_CONFIRM_VERSION4': True, + 'DJANGOCMS_VERSIONING_LOCK_VERSIONS': True, } diff --git a/tests/requirements/dj32_cms40.txt b/tests/requirements/dj32_cms40.txt deleted file mode 100644 index 7c8f1d3..0000000 --- a/tests/requirements/dj32_cms40.txt +++ /dev/null @@ -1,9 +0,0 @@ --r requirements_base.txt - -Django>=3.2,<4.0 - -# Unreleased django-cms 4.0 compatible packages -https://github.com/django-cms/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor -https://github.com/django-cms/djangocms-versioning/tarball/1.2.2#egg=djangocms-versioning -https://github.com/django-cms/djangocms-alias/tarball/1.11.0#egg=djangocms-alias -https://github.com/django-cms/djangocms-moderation/tarball/2.1.5#egg=djangocms-moderation \ No newline at end of file diff --git a/tests/requirements/dj42_cms40.txt b/tests/requirements/dj42_cms40.txt deleted file mode 100644 index 69bdcc2..0000000 --- a/tests/requirements/dj42_cms40.txt +++ /dev/null @@ -1,8 +0,0 @@ --r requirements_base.txt - -Django>=4.2,<5.0 - -https://github.com/django-cms/djangocms-text-ckeditor/tarball/5.1.5#egg=djangocms-text-ckeditor -https://github.com/django-cms/djangocms-versioning/tarball/support/django-cms-4.0.x#egg=djangocms-versioning -https://github.com/django-cms/djangocms-alias/tarball/support/django-cms-4.0.x#egg=djangocms-alias -https://github.com/django-cms/djangocms-moderation/tarball/master#egg=djangocms-moderation \ No newline at end of file diff --git a/tests/requirements/dj42_cms41.txt b/tests/requirements/dj42_cms41.txt new file mode 100644 index 0000000..e8b5b86 --- /dev/null +++ b/tests/requirements/dj42_cms41.txt @@ -0,0 +1,9 @@ +-r requirements_base.txt + +Django>=4.2,<5.0 +django-cms>=4.1.0 +djangocms-versioning==2.1.0 +djangocms-text-ckeditor==5.1.5 + +https://github.com/django-cms/djangocms-alias/tarball/master#egg=djangocms-alias +https://github.com/django-cms/djangocms-moderation/tarball/master#egg=djangocms-moderation \ No newline at end of file diff --git a/tests/requirements/requirements_base.txt b/tests/requirements/requirements_base.txt index ed7b5b3..6e74fbc 100644 --- a/tests/requirements/requirements_base.txt +++ b/tests/requirements/requirements_base.txt @@ -6,6 +6,4 @@ factory-boy flake8 isort mock -pyflakes>=2.1.1 - -https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms \ No newline at end of file +pyflakes>=2.1.1 \ No newline at end of file diff --git a/tests/test_admin.py b/tests/test_admin.py deleted file mode 100644 index 3d2a66b..0000000 --- a/tests/test_admin.py +++ /dev/null @@ -1,207 +0,0 @@ -from unittest import skip -from unittest.mock import patch - -from django.contrib import admin -from django.test import RequestFactory -from django.utils.translation import gettext_lazy as _ - -from cms.test_utils.testcases import CMSTestCase - -from djangocms_alias.models import Alias, AliasContent, Category -from djangocms_versioning import admin as versioning_admin -from djangocms_versioning.constants import DRAFT, PUBLISHED -from djangocms_versioning.models import Version - -import djangocms_version_locking.helpers -from djangocms_version_locking.admin import VersionLockAdminMixin -from djangocms_version_locking.helpers import ( - replace_admin_for_models, - version_lock_admin_factory, -) -from djangocms_version_locking.test_utils import factories -from djangocms_version_locking.test_utils.polls.cms_config import ( - PollsCMSConfig, -) -from djangocms_version_locking.test_utils.polls.models import ( - Answer, - Poll, - PollContent, -) - - -class AdminReplaceVersioningTestCase(CMSTestCase): - - def setUp(self): - self.model = Poll - self.site = admin.AdminSite() - self.admin_class = type('TestAdmin', (admin.ModelAdmin, ), {}) - - def test_replace_admin_on_unregistered_model(self): - """Test that calling `replace_admin_for_models` with a model that - isn't registered in admin is a no-op. - """ - replace_admin_for_models([self.model], self.site) - - self.assertNotIn(self.model, self.site._registry) - - def test_replace_admin_on_registered_models_default_site(self): - with patch.object(djangocms_version_locking.helpers, '_replace_admin_for_model') as mock: - replace_admin_for_models([PollContent]) - - mock.assert_called_with(admin.site._registry[PollContent], admin.site) - - def test_replace_admin_on_registered_models(self): - self.site.register(self.model, self.admin_class) - self.site.register(Answer, self.admin_class) - models = [self.model, Answer] - - replace_admin_for_models(models, self.site) - - for model in models: - self.assertIn(model, self.site._registry) - self.assertIn(self.admin_class, self.site._registry[model].__class__.mro()) - self.assertIn(VersionLockAdminMixin, self.site._registry[model].__class__.mro()) - - def test_replace_default_admin_on_registered_model(self): - """Test that registering a model without specifying own - ModelAdmin class still results in overridden admin class. - """ - self.site.register(self.model) - - replace_admin_for_models([self.model], self.site) - - self.assertIn(self.model, self.site._registry) - self.assertIn(VersionLockAdminMixin, self.site._registry[self.model].__class__.mro()) - - def test_replace_admin_again(self): - """Test that, if a model's admin class already subclasses - VersionLockAdminMixin, nothing happens. - """ - version_admin = version_lock_admin_factory(self.admin_class) - self.site.register(self.model, version_admin) - - replace_admin_for_models([self.model], self.site) - - self.assertIn(self.model, self.site._registry) - self.assertEqual(self.site._registry[self.model].__class__, version_admin) - - -class AdminLockedFieldTestCase(CMSTestCase): - - def setUp(self): - site = admin.AdminSite() - self.hijacked_admin = versioning_admin.VersionAdmin(Version, site) - - def test_version_admin_contains_locked_field(self): - """ - The locked column exists in the admin field list - """ - request = RequestFactory().get('/admin/djangocms_versioning/pollcontentversion/') - self.assertIn(_("locked"), self.hijacked_admin.get_list_display(request)) - - def test_version_lock_state_locked(self): - """ - A published version does not have an entry in the locked column in the admin - """ - published_version = factories.PollVersionFactory(state=PUBLISHED) - - self.assertEqual("", self.hijacked_admin.locked(published_version)) - - def test_version_lock_state_unlocked(self): - """ - A draft version does have an entry in the locked column in the and is not empty - """ - draft_version = factories.PollVersionFactory(state=DRAFT) - - self.assertNotEqual("", self.hijacked_admin.locked(draft_version)) - - -class AdminPermissionTestCase(CMSTestCase): - - @classmethod - def setUpTestData(cls): - cls.versionable = PollsCMSConfig.versioning[0] - - def setUp(self): - self.superuser = self.get_superuser() - self.user_has_change_perms = self._create_user( - "user_has_unlock_perms", - is_staff=True, - permissions=["change_pollcontentversion", "delete_versionlock"], - ) - - @skip("FIXME: Oddly this test runs and passes fine locally but fails when ran in the CI!") - def test_user_has_change_permission(self): - """ - The user who created the version has permission to change it - """ - content = factories.PollVersionFactory(state=DRAFT, created_by=self.user_has_change_perms) - url = self.get_admin_url(self.versionable.version_model_proxy, 'change', content.pk) - - with self.login_user_context(self.user_has_change_perms): - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - - @skip("FIXME: This test should try and submit changes to the item as the form renders as readonly currently.") - def test_user_does_not_have_change_permission(self): - """ - A different user from the user who created - the version does not have permission to change it - """ - author = factories.UserFactory(is_staff=True) - content = factories.PollVersionFactory(state=DRAFT, created_by=author) - url = self.get_admin_url(self.versionable.version_model_proxy, 'change', content.pk) - - with self.login_user_context(self.user_has_change_perms): - response = self.client.get(url) - - self.assertEqual(response.status_code, 403) - - -class AdminExtensionTestCase(CMSTestCase): - def setUp(self): - self.superuser = self.get_superuser() - self.regular_staff_user = self._create_user( - "regular", - is_staff=True, - permissions=["view_aliascontent", "change_aliascontent"] - ) - self.category = Category.objects.create(name='Language Filter Category') - self.alias = Alias.objects.create( - category=self.category, - position=0, - ) - self.alias_content = AliasContent.objects.create( - alias=self.alias, - name="Alias Content", - language="en", - ) - - def test_version_lock_added_to_locked_alias(self): - """ - With a locked item, the ExtendedVersionAdminMixin injects the lock item to the name field - """ - Version.objects.create(content=self.alias_content, created_by=self.superuser, state=DRAFT) - admin_url = self.get_admin_url(AliasContent, "changelist") - - with self.login_user_context(self.regular_staff_user): - response = self.client.get(admin_url) - - self.assertContains(response, '') - self.assertContains(response, '') - self.assertContains(response, self.alias_content.name) - - def test_version_lock_not_added_to_unlocked_alias(self): - """ - With an unlocked item, the ExtendedVersionAdminMixin just displays the Alias name - """ - Version.objects.create(content=self.alias_content, created_by=self.superuser, state=PUBLISHED) - admin_url = self.get_admin_url(AliasContent, "changelist") - - with self.login_user_context(self.superuser): - response = self.client.get(admin_url) - - self.assertNotContains(response, '') - self.assertNotContains(response, '') - self.assertContains(response, self.alias_content.name) diff --git a/tests/test_admin_monkey_patch.py b/tests/test_admin_monkey_patch.py deleted file mode 100644 index 7c282b1..0000000 --- a/tests/test_admin_monkey_patch.py +++ /dev/null @@ -1,292 +0,0 @@ -from unittest import skip - -from django.contrib import admin -from django.contrib.contenttypes.models import ContentType -from django.template.loader import render_to_string -from django.test import RequestFactory - -from cms.test_utils.testcases import CMSTestCase - -from djangocms_versioning import constants -from djangocms_versioning.helpers import version_list_url -from djangocms_versioning.models import Version - -from djangocms_version_locking.models import VersionLock -from djangocms_version_locking.test_utils import factories -from djangocms_version_locking.test_utils.polls.cms_config import ( - PollsCMSConfig, -) - - -def _content_has_lock(content): - """ - Check for a lock entry from content - """ - try: - VersionLock.objects.get( - version__content_type=ContentType.objects.get_for_model(content), - version__object_id=content.pk, - ) - except VersionLock.DoesNotExist: - return False - return True - - -class VersionLockUnlockTestCase(CMSTestCase): - - @classmethod - def setUpTestData(cls): - cls.versionable = PollsCMSConfig.versioning[0] - cls.default_permissions = ["change_pollcontentversion"] - - def setUp(self): - self.superuser = self.get_superuser() - self.user_author = self._create_user( - "author", - is_staff=True, - permissions=self.default_permissions, - ) - self.user_has_no_unlock_perms = self._create_user( - "user_has_no_unlock_perms", - is_staff=True, - permissions=self.default_permissions, - ) - self.user_has_unlock_perms = self._create_user( - "user_has_unlock_perms", - is_staff=True, - permissions=["delete_versionlock"] + self.default_permissions, - ) - - def test_unlock_view_redirects_404_when_not_draft(self): - poll_version = factories.PollVersionFactory(state=constants.PUBLISHED, created_by=self.superuser) - unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', poll_version.pk) - - # 404 when not in draft - with self.login_user_context(self.superuser): - response = self.client.post(unlock_url, follow=True) - - self.assertEqual(response.status_code, 404) - - def test_unlock_view_not_possible_for_user_with_no_permissions(self): - poll_version = factories.PollVersionFactory(state=constants.DRAFT, created_by=self.user_author) - unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', poll_version.pk) - - with self.login_user_context(self.user_has_no_unlock_perms): - response = self.client.post(unlock_url, follow=True) - - self.assertEqual(response.status_code, 403) - - # Fetch the latest state of this version - updated_poll_version = Version.objects.get(pk=poll_version.pk) - - # The version is still locked - self.assertTrue(hasattr(updated_poll_version, 'versionlock')) - # The author is unchanged - self.assertEqual(updated_poll_version.versionlock.created_by, self.user_author) - - def test_unlock_view_possible_for_user_with_permissions(self): - poll_version = factories.PollVersionFactory(state=constants.DRAFT, created_by=self.user_author) - unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', poll_version.pk) - - with self.login_user_context(self.user_has_unlock_perms): - response = self.client.post(unlock_url, follow=True) - - self.assertEqual(response.status_code, 200) - - # Fetch the latest state of this version - updated_poll_version = Version.objects.get(pk=poll_version.pk) - - # The version is not locked - self.assertFalse(hasattr(updated_poll_version, 'versionlock')) - - @skip("Requires clarification if this is still a valid requirement!") - def test_unlock_link_not_present_for_author(self): - # FIXME: May be redundant now as this requirement was probably removed at a later date due - # to the fact that an author may be asked to unlock their version for someone else to use! - author = self.get_superuser() - poll_version = factories.PollVersionFactory(state=constants.DRAFT, created_by=author) - changelist_url = version_list_url(poll_version.content) - unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', poll_version.pk) - unlock_control = render_to_string( - 'djangocms_version_locking/admin/unlock_icon.html', - {'unlock_url': unlock_url} - ) - - with self.login_user_context(author): - response = self.client.get(changelist_url) - - self.assertNotContains(response, unlock_control, html=True) - - def test_unlock_link_not_present_for_user_with_no_unlock_privileges(self): - poll_version = factories.PollVersionFactory(state=constants.DRAFT, created_by=self.user_author) - changelist_url = version_list_url(poll_version.content) - unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', poll_version.pk) - unlock_control = render_to_string( - 'djangocms_version_locking/admin/unlock_icon.html', - {'unlock_url': unlock_url} - ) - - with self.login_user_context(self.user_has_no_unlock_perms): - response = self.client.post(changelist_url) - - self.assertNotContains(response, unlock_control, html=True) - - def test_unlock_link_present_for_user_with_privileges(self): - poll_version = factories.PollVersionFactory(state=constants.DRAFT, created_by=self.user_author) - changelist_url = version_list_url(poll_version.content) - unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', poll_version.pk) - unlock_control = render_to_string( - 'djangocms_version_locking/admin/unlock_icon.html', - {'unlock_url': unlock_url} - ) - - with self.login_user_context(self.user_has_unlock_perms): - response = self.client.post(changelist_url) - - self.assertContains(response, unlock_control, html=True) - - def test_unlock_link_only_present_for_draft_versions(self): - draft_version = factories.PollVersionFactory(created_by=self.user_author) - published_version = Version.objects.create( - content=factories.PollContentFactory(poll=draft_version.content.poll), - created_by=factories.UserFactory(), - state=constants.PUBLISHED - ) - draft_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', draft_version.pk) - draft_unlock_control = render_to_string( - 'djangocms_version_locking/admin/unlock_icon.html', - {'unlock_url': draft_unlock_url} - ) - published_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, 'unlock', published_version.pk) - published_unlock_control = render_to_string( - 'djangocms_version_locking/admin/unlock_icon.html', - {'unlock_url': published_unlock_url} - ) - changelist_url = version_list_url(draft_version.content) - - with self.login_user_context(self.superuser): - response = self.client.post(changelist_url) - - # The draft version unlock control exists - self.assertContains(response, draft_unlock_control, html=True) - # The published version exists - self.assertNotContains(response, published_unlock_control, html=True) - - def test_unlock_and_new_user_edit_creates_version_lock(self): - """ - When a version is unlocked a different user (or the same) can then visit the edit link and take - ownership of the version, this creates a version lock for the editing user - """ - draft_version = factories.PollVersionFactory(created_by=self.user_author) - draft_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, - 'unlock', draft_version.pk) - - # The version is owned by the author - self.assertEqual(draft_version.created_by, self.user_author) - # The version lock exists and is owned by the author - self.assertEqual(draft_version.versionlock.created_by, self.user_author) - - # Unlock the version with a different user with unlock permissions - with self.login_user_context(self.user_has_unlock_perms): - self.client.post(draft_unlock_url, follow=True) - - updated_draft_version = Version.objects.get(pk=draft_version.pk) - updated_draft_edit_url = self.get_admin_url( - self.versionable.version_model_proxy, - 'edit_redirect', updated_draft_version.pk - ) - - # The version is still owned by the author - self.assertTrue(updated_draft_version.created_by, self.user_author) - # The version lock does not exist - self.assertFalse(hasattr(updated_draft_version, 'versionlock')) - - # Visit the edit page with a user without unlock permissions - with self.login_user_context(self.user_has_no_unlock_perms): - self.client.post(updated_draft_edit_url) - - updated_draft_version = Version.objects.get(pk=draft_version.pk) - - # The version is now owned by the user with no permissions - self.assertTrue(updated_draft_version.created_by, self.user_has_no_unlock_perms) - # The version lock exists and is now owned by the user with no permissions - self.assertEqual(updated_draft_version.versionlock.created_by, self.user_has_no_unlock_perms) - - -class VersionLockEditActionStateTestCase(CMSTestCase): - - def setUp(self): - self.superuser = self.get_superuser() - self.user_author = self._create_user("author", is_staff=True, is_superuser=False) - self.versionable = PollsCMSConfig.versioning[0] - self.version_admin = admin.site._registry[self.versionable.version_model_proxy] - - def test_edit_action_link_enabled_state(self): - """ - The edit action is active - """ - version = factories.PollVersionFactory(created_by=self.user_author) - author_request = RequestFactory() - author_request.user = self.user_author - otheruser_request = RequestFactory() - otheruser_request.user = self.superuser - - actual_enabled_state = self.version_admin._get_edit_link(version, author_request) - - self.assertNotIn("inactive", actual_enabled_state) - - def test_edit_action_link_disabled_state(self): - """ - The edit action is disabled for a different user to the locked user - """ - version = factories.PollVersionFactory(created_by=self.user_author) - author_request = RequestFactory() - author_request.user = self.user_author - otheruser_request = RequestFactory() - otheruser_request.user = self.superuser - actual_disabled_state = self.version_admin._get_edit_link(version, otheruser_request) - - self.assertIn("inactive", actual_disabled_state) - - -class VersionLockEditActionSideFrameTestCase(CMSTestCase): - def setUp(self): - self.superuser = self.get_superuser() - self.user_author = self._create_user("author", is_staff=True, is_superuser=False) - self.versionable = PollsCMSConfig.versioning[0] - self.version_admin = admin.site._registry[self.versionable.version_model_proxy] - - def test_version_unlock_keep_side_frame(self): - """ - When clicking on an versionables enabled unlock icon, the sideframe is kept open - """ - version = factories.PollVersionFactory(created_by=self.user_author) - author_request = RequestFactory() - author_request.user = self.user_author - otheruser_request = RequestFactory() - otheruser_request.user = self.superuser - - actual_enabled_state = self.version_admin._get_unlock_link(version, otheruser_request) - - # The url link should keep the sideframe open - self.assertIn("js-versioning-keep-sideframe", actual_enabled_state) - self.assertNotIn("js-versioning-close-sideframe", actual_enabled_state) - - -class VersionLockMediaMonkeyPatchTestCase(CMSTestCase): - - def setUp(self): - self.superuser = self.get_superuser() - - def test_version_locking_css_media_loaded(self): - """ - The verison locking css media is loaded on the page - """ - poll_version = factories.PollVersionFactory(created_by=self.superuser) - changelist_url = version_list_url(poll_version.content) - css_file = "djangocms_version_locking/css/version-locking.css" - - with self.login_user_context(self.superuser): - response = self.client.post(changelist_url) - - self.assertContains(response, css_file) diff --git a/tests/test_check.py b/tests/test_check.py deleted file mode 100644 index 3335516..0000000 --- a/tests/test_check.py +++ /dev/null @@ -1,53 +0,0 @@ -from cms.models.fields import PlaceholderRelationField -from cms.test_utils.testcases import CMSTestCase - -from djangocms_versioning.constants import ARCHIVED -from djangocms_versioning.test_utils.factories import ( - FancyPollFactory, - PageVersionFactory, - PlaceholderFactory, -) - -from djangocms_version_locking.helpers import ( - placeholder_content_is_unlocked_for_user, -) - - -class CheckLockTestCase(CMSTestCase): - - def test_check_no_lock(self): - user = self.get_superuser() - version = PageVersionFactory(state=ARCHIVED) - placeholder = PlaceholderFactory(source=version.content) - - self.assertTrue(placeholder_content_is_unlocked_for_user(placeholder, user)) - - def test_check_locked_for_the_same_user(self): - user = self.get_superuser() - version = PageVersionFactory(created_by=user) - placeholder = PlaceholderFactory(source=version.content) - - self.assertTrue(placeholder_content_is_unlocked_for_user(placeholder, user)) - - def test_check_locked_for_the_other_user(self): - user1 = self.get_superuser() - user2 = self.get_standard_user() - version = PageVersionFactory(created_by=user1) - placeholder = PlaceholderFactory(source=version.content) - - self.assertFalse(placeholder_content_is_unlocked_for_user(placeholder, user2)) - - def test_check_no_lock_for_unversioned_model(self): - user2 = self.get_standard_user() - placeholder = PlaceholderFactory(source=FancyPollFactory()) - - self.assertTrue(placeholder_content_is_unlocked_for_user(placeholder, user2)) - - -class CheckInjectTestCase(CMSTestCase): - - def test_lock_check_is_injected_into_default_checks(self): - self.assertIn( - placeholder_content_is_unlocked_for_user, - PlaceholderRelationField.default_checks, - ) diff --git a/tests/test_emails.py b/tests/test_emails.py deleted file mode 100644 index 8be6aa4..0000000 --- a/tests/test_emails.py +++ /dev/null @@ -1,129 +0,0 @@ -from django.contrib.auth.models import Permission -from django.core import mail -from django.utils.translation import gettext_lazy as _ - -from cms.test_utils.testcases import CMSTestCase -from cms.toolbar.utils import get_object_preview_url -from cms.utils import get_current_site - -from djangocms_versioning.cms_config import VersioningCMSConfig -from djangocms_versioning.test_utils import factories - -from djangocms_version_locking.utils import get_absolute_url - - -class VersionLockNotificationEmailsTestCase(CMSTestCase): - - def setUp(self): - self.superuser = self.get_superuser() - self.user_author = self._create_user("author", is_staff=True, is_superuser=False) - self.user_has_no_perms = self._create_user("user_has_no_perms", is_staff=True, is_superuser=False) - self.user_has_unlock_perms = self._create_user("user_has_unlock_perms", is_staff=True, is_superuser=False) - self.versionable = VersioningCMSConfig.versioning[0] - - # Set permissions - delete_permission = Permission.objects.get(codename='delete_versionlock') - self.user_has_unlock_perms.user_permissions.add(delete_permission) - - def test_notify_version_author_version_unlocked_email_sent_for_different_user(self): - """ - The user unlocking a version that is authored buy a different user - should be sent a notification email - """ - draft_version = factories.PageVersionFactory(content__template="", created_by=self.user_author) - draft_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, - 'unlock', draft_version.pk) - - # Check that no emails exist - self.assertEqual(len(mail.outbox), 0) - - # Unlock the version with a different user with unlock permissions - with self.login_user_context(self.user_has_unlock_perms): - self.client.post(draft_unlock_url, follow=True) - - site = get_current_site() - expected_subject = "[Django CMS] ({site_name}) {title} - {description}".format( - site_name=site.name, - title=draft_version.content, - description=_("Unlocked"), - ) - expected_body = "The following draft version has been unlocked by {by_user} for their use.".format( - by_user=self.user_has_unlock_perms - ) - expected_version_url = get_absolute_url( - get_object_preview_url(draft_version.content) - ) - - self.assertEqual(len(mail.outbox), 1) - self.assertEqual(mail.outbox[0].subject, expected_subject) - self.assertEqual(mail.outbox[0].to[0], self.user_author.email) - self.assertTrue(expected_body in mail.outbox[0].body) - self.assertTrue(expected_version_url in mail.outbox[0].body) - - def test_notify_version_author_version_unlocked_email_not_sent_for_different_user(self): - """ - The user unlocking a version that authored the version should not be - sent a notification email - """ - draft_version = factories.PageVersionFactory(content__template="", created_by=self.user_author) - draft_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, - 'unlock', draft_version.pk) - - # Check that no emails exist - self.assertEqual(len(mail.outbox), 0) - - # Unlock the version the same user who authored it - with self.login_user_context(self.user_author): - self.client.post(draft_unlock_url, follow=True) - - # Check that no emails still exist - self.assertEqual(len(mail.outbox), 0) - - def test_notify_version_author_version_unlocked_email_contents_users_full_name_used(self): - """ - The email contains the full name of the author - """ - user = self.user_has_unlock_perms - user.first_name = "Firstname" - user.last_name = "Lastname" - user.save() - draft_version = factories.PageVersionFactory(content__template="", created_by=self.user_author) - draft_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, - 'unlock', draft_version.pk) - - # Check that no emails exist - self.assertEqual(len(mail.outbox), 0) - - # Unlock the version with a different user with unlock permissions - with self.login_user_context(user): - self.client.post(draft_unlock_url, follow=True) - - expected_body = "The following draft version has been unlocked by {by_user} for their use.".format( - by_user=user.get_full_name() - ) - - self.assertEqual(len(mail.outbox), 1) - self.assertTrue(expected_body in mail.outbox[0].body) - - def test_notify_version_author_version_unlocked_email_contents_users_username_used(self): - """ - The email contains the username of the author because no name is available - """ - user = self.user_has_unlock_perms - draft_version = factories.PageVersionFactory(content__template="", created_by=self.user_author) - draft_unlock_url = self.get_admin_url(self.versionable.version_model_proxy, - 'unlock', draft_version.pk) - - # Check that no emails exist - self.assertEqual(len(mail.outbox), 0) - - # Unlock the version with a different user with unlock permissions - with self.login_user_context(user): - self.client.post(draft_unlock_url, follow=True) - - expected_body = "The following draft version has been unlocked by {by_user} for their use.".format( - by_user=user.username - ) - - self.assertEqual(len(mail.outbox), 1) - self.assertTrue(expected_body in mail.outbox[0].body) diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index c9fa8c5..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,111 +0,0 @@ -from cms.test_utils.testcases import CMSTestCase - -from djangocms_versioning import constants -from djangocms_versioning.models import Version - -from djangocms_version_locking.helpers import version_is_locked -from djangocms_version_locking.test_utils import factories -from djangocms_version_locking.test_utils.polls.cms_config import ( - PollsCMSConfig, -) - - -class TestVersionsLockTestCase(CMSTestCase): - - def setUp(self): - self.versionable = PollsCMSConfig.versioning[0] - - def test_version_is_locked_for_draft(self): - """ - A version lock is present when a content version is in a draft state - """ - draft_version = factories.PollVersionFactory(state=constants.DRAFT) - - self.assertTrue(hasattr(draft_version, 'versionlock')) - - def test_version_is_unlocked_for_publishing(self): - """ - A version lock is not present when a content version is in a published or unpublished state - """ - poll_version = factories.PollVersionFactory(state=constants.DRAFT) - publish_url = self.get_admin_url(self.versionable.version_model_proxy, 'publish', poll_version.pk) - unpublish_url = self.get_admin_url(self.versionable.version_model_proxy, 'unpublish', poll_version.pk) - user = self.get_staff_user_with_no_permissions() - - with self.login_user_context(user): - self.client.post(publish_url) - - updated_poll_version = Version.objects.get(pk=poll_version.pk) - - # The state is now PUBLISHED - self.assertEqual(updated_poll_version.state, constants.PUBLISHED) - # Version lock does not exist - self.assertFalse(hasattr(updated_poll_version, 'versionlock')) - - with self.login_user_context(user): - self.client.post(unpublish_url) - - updated_poll_version = Version.objects.get(pk=poll_version.pk) - - # The state is now UNPUBLISHED - self.assertEqual(updated_poll_version.state, constants.UNPUBLISHED) - # Version lock does not exist - self.assertFalse(hasattr(updated_poll_version, 'versionlock')) - - def test_version_is_unlocked_for_archived(self): - """ - A version lock is not present when a content version is in an archived state - """ - poll_version = factories.PollVersionFactory(state=constants.DRAFT) - archive_url = self.get_admin_url(self.versionable.version_model_proxy, 'archive', poll_version.pk) - version_lock = version_is_locked(poll_version) - user = self.get_superuser() - version_lock.created_by = user - version_lock.save() - - with self.login_user_context(user): - self.client.post(archive_url) - - updated_poll_version = Version.objects.get(pk=poll_version.pk) - - # The state is now ARCHIVED - self.assertEqual(updated_poll_version.state, constants.ARCHIVED) - # Version lock does not exist - self.assertFalse(hasattr(updated_poll_version, 'versionlock')) - - -class TestVersionCopyLocks(CMSTestCase): - - def test_draft_version_copy_creates_draft_lock(self): - """ - A version lock is created for a new draft version copied from a draft version - """ - user = factories.UserFactory() - draft_version = factories.PollVersionFactory(state=constants.DRAFT) - new_version = draft_version.copy(user) - - self.assertTrue(hasattr(new_version, 'versionlock')) - - def test_published_version_copy_creates_draft_lock(self): - """ - A version lock is created for a published version copied from a draft version - """ - user = factories.UserFactory() - published_version = factories.PollVersionFactory(state=constants.PUBLISHED) - new_version = published_version.copy(user) - - self.assertTrue(hasattr(new_version, 'versionlock')) - - def test_version_copy_adds_correct_locked_user(self): - """ - A copied version creates a lock for the user that copied the version. - The users should not be the same. - """ - original_user = factories.UserFactory() - original_version = factories.PollVersionFactory(created_by=original_user) - copy_user = factories.UserFactory() - copied_version = original_version.copy(copy_user) - - self.assertNotEqual(original_user, copy_user) - self.assertEqual(original_version.versionlock.created_by, original_user) - self.assertEqual(copied_version.versionlock.created_by, copy_user) diff --git a/tests/test_models_monkeypatch.py b/tests/test_models_monkeypatch.py deleted file mode 100644 index 854882f..0000000 --- a/tests/test_models_monkeypatch.py +++ /dev/null @@ -1,102 +0,0 @@ -from cms.test_utils.testcases import CMSTestCase -from cms.utils.urlutils import add_url_parameters - -from djangocms_moderation.models import ModerationRequest -from djangocms_moderation.utils import get_admin_url -from djangocms_versioning.test_utils.factories import PageVersionFactory - -from djangocms_version_locking.helpers import ( - remove_version_lock, - version_is_locked, -) -from djangocms_version_locking.test_utils.factories import ( - ModerationCollectionFactory, - PlaceholderFactory, - PollPluginFactory, - PollVersionFactory, - UserFactory, -) - - -class ModerationCollectionTestCase(CMSTestCase): - def setUp(self): - self.language = "en" - self.user_1 = self.get_superuser() - self.user_2 = UserFactory() - self.collection = ModerationCollectionFactory(author=self.user_1) - self.page_version = PageVersionFactory(created_by=self.user_1) - self.placeholder = PlaceholderFactory(source=self.page_version.content) - self.poll_version = PollVersionFactory(created_by=self.user_2, content__language=self.language) - - def test_add_version_with_locked_plugins(self): - """ - Locked plugins should not be allowed to be added to a collection - """ - PollPluginFactory(placeholder=self.placeholder, poll=self.poll_version.content.poll) - - admin_endpoint = get_admin_url( - name="cms_moderation_items_to_collection", language="en", args=() - ) - - url = add_url_parameters( - admin_endpoint, - return_to_url="http://example.com", - version_ids=self.page_version.pk, - collection_id=self.collection.pk, - ) - - # Poll should be locked by default - poll_is_locked = version_is_locked(self.poll_version) - self.assertTrue(poll_is_locked) - - with self.login_user_context(self.user_1): - self.client.post( - path=url, - data={"collection": self.collection.pk, "versions": [self.page_version.pk]}, - follow=False, - ) - - # Get all moderation request objects for the collection - moderation_requests = ModerationRequest.objects.filter(collection=self.collection) - - self.assertEqual(moderation_requests.count(), 1) - self.assertTrue(moderation_requests.filter(version=self.page_version).exists()) - self.assertFalse(moderation_requests.filter(version=self.poll_version).exists()) - - def test_add_version_with_unlocked_child(self): - """ - Only plugins that are unlocked should be added to collection - """ - - PollPluginFactory(placeholder=self.placeholder, poll=self.poll_version.content.poll) - - admin_endpoint = get_admin_url( - name="cms_moderation_items_to_collection", language="en", args=() - ) - - url = add_url_parameters( - admin_endpoint, - return_to_url="http://example.com", - version_ids=self.page_version.pk, - collection_id=self.collection.pk, - ) - - # Poll should be locked by default - poll_is_locked = version_is_locked(self.poll_version) - self.assertTrue(poll_is_locked) - - # Unlock the poll version - remove_version_lock(self.poll_version) - - with self.login_user_context(self.user_1): - self.client.post( - path=url, - data={"collection": self.collection.pk, "versions": [self.page_version.pk]}, - follow=False, - ) - - # Get all moderation request objects for the collection - moderation_requests = ModerationRequest.objects.filter(collection=self.collection) - self.assertEqual(moderation_requests.count(), 2) - self.assertTrue(moderation_requests.filter(version=self.page_version).exists()) - self.assertTrue(moderation_requests.filter(version=self.poll_version).exists()) diff --git a/tests/test_toolbar_monkeypatch.py b/tests/test_toolbar_monkeypatch.py index fd7616d..715d9d7 100644 --- a/tests/test_toolbar_monkeypatch.py +++ b/tests/test_toolbar_monkeypatch.py @@ -75,7 +75,7 @@ def test_enable_edit_button_when_content_is_locked(self): self.assertFalse(edit_button.disabled) self.assertListEqual( edit_button.extra_classes, - ['cms-btn-action', 'cms-versioning-js-edit-btn'] + ['cms-btn-action', 'cms-form-post-method', 'cms-versioning-js-edit-btn'] ) def test_edit_button_when_content_is_locked_users_full_name_used(self): diff --git a/tox.ini b/tox.ini index 2feff8f..81944bf 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = flake8 isort - py{38,39,310}-dj{32,42}-sqlite-cms40 + py{39,310,311}-dj{42}-sqlite-cms41 skip_missing_interpreters=True @@ -11,13 +11,12 @@ deps = flake8: -r{toxinidir}/tests/requirements/requirements_base.txt isort: -r{toxinidir}/tests/requirements/requirements_base.txt - dj32: -r{toxinidir}/tests/requirements/dj32_cms40.txt - dj42: -r{toxinidir}/tests/requirements/dj42_cms40.txt + dj42: -r{toxinidir}/tests/requirements/dj42_cms41.txt basepython = - py38: python3.8 py39: python3.9 py310: python3.10 + py311: python3.11 commands = {envpython} --version @@ -27,8 +26,8 @@ commands = [testenv:flake8] commands = flake8 -basepython = python3.9 +basepython = python3.11 [testenv:isort] commands = isort --recursive --check-only --diff {toxinidir} -basepython = python3.9 +basepython = python3.11