Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] django-cms 4.1 compatible for version locking #85

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
25c946b
initial change made for djangocms 4.1 compatible
Aug 8, 2024
6e42193
add testcases changes
Aug 8, 2024
e12fa57
removed model patch from locking
Aug 12, 2024
6d9d4eb
remove Version.admin.locked monkeypatch.
FreemanPancake Aug 14, 2024
152e980
make state indicator readonly, and add more function to alias action …
FreemanPancake Aug 29, 2024
ee2835b
remove edit link and code format
FreemanPancake Aug 29, 2024
4fb1d10
hide delete button for alias, as this will take over by unpublish/arc…
FreemanPancake Aug 29, 2024
c3a2d27
Add 'Manage versions' action to versioned admin's action list, State …
FreemanPancake Oct 22, 2024
abbad0d
add alias edit btn
FreemanPancake Oct 25, 2024
acd9871
fix Alias language filer selector UI issue.
FreemanPancake Oct 25, 2024
e96aa25
chore: update djangocms-versioning to 2.1.0 in `dj42_cms41`
Oct 31, 2024
a7241fa
fix: flake8 & isort issues
Oct 31, 2024
9a18bf4
chore: remove python 3.8 support
Oct 31, 2024
bb454b2
docs: update changelog
Oct 31, 2024
9b50f0a
fix: duplicated manage-version link & update version icon
Nov 4, 2024
c448c9a
fix: flake8 & isort issue
Nov 4, 2024
09e62ad
fix empty date issue if the alias has legacy empty date.
FreemanPancake Nov 4, 2024
80e68fe
fix empty date issue if the alias has legacy empty data, code refact.
FreemanPancake Nov 4, 2024
c45768e
add locked_by data migration file.
FreemanPancake Nov 7, 2024
99acc78
fix: refine 0002 migration file
Nov 7, 2024
e8bf295
chore: delete `VersionLock` model and generate migration file
Nov 7, 2024
9275657
chore: comment models
Nov 8, 2024
17260f6
add AdminActionListMixin to support action list burger menu.
FreemanPancake Nov 18, 2024
6367dd8
monkeypatch ChangeListActionsMixin in cms.admin.utils to support acti…
FreemanPancake Nov 19, 2024
005cda9
code format
FreemanPancake Nov 19, 2024
4e19abf
fix: actions.js get img tag.
FreemanPancake Nov 19, 2024
6d89ce1
fix: get the correct base admin class of djangocms_alias
Nov 19, 2024
25dddce
fix: remove duplicated media js from monkeypatch.
FreemanPancake Nov 19, 2024
6913e0c
fix: isort issue
Nov 19, 2024
64affce
fix: action list layout issue.
FreemanPancake Nov 20, 2024
34e9d01
fix: migration file
Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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:
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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)
==================
25 changes: 0 additions & 25 deletions djangocms_version_locking/admin.py

This file was deleted.

2 changes: 1 addition & 1 deletion djangocms_version_locking/apps.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 14 additions & 8 deletions djangocms_version_locking/cms_config.py
Original file line number Diff line number Diff line change
@@ -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('<span class="cms-icon cms-icon-lock"></span>')
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
6 changes: 0 additions & 6 deletions djangocms_version_locking/conf.py

This file was deleted.

40 changes: 0 additions & 40 deletions djangocms_version_locking/emails.py

This file was deleted.

168 changes: 0 additions & 168 deletions djangocms_version_locking/helpers.py

This file was deleted.

28 changes: 28 additions & 0 deletions djangocms_version_locking/migrations/0002_migrate_locked_by.py
Original file line number Diff line number Diff line change
@@ -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)
]
16 changes: 16 additions & 0 deletions djangocms_version_locking/migrations/0003_delete_versionlock.py
Original file line number Diff line number Diff line change
@@ -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',
),
]
35 changes: 19 additions & 16 deletions djangocms_version_locking/models.py
Original file line number Diff line number Diff line change
@@ -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')
# )
214 changes: 61 additions & 153 deletions djangocms_version_locking/monkeypatch/admin.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 0 additions & 8 deletions djangocms_version_locking/monkeypatch/checks.py

This file was deleted.

11 changes: 4 additions & 7 deletions djangocms_version_locking/monkeypatch/cms_toolbars.py
Original file line number Diff line number Diff line change
@@ -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.
Empty file.
97 changes: 97 additions & 0 deletions djangocms_version_locking/monkeypatch/djangocms_alias/admin.py
Original file line number Diff line number Diff line change
@@ -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)
102 changes: 0 additions & 102 deletions djangocms_version_locking/monkeypatch/models.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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 = '<input type="hidden" name="csrfmiddlewaretoken" value="' + document.cookie.match(/csrftoken=([^;]*);?/)[1] + '">';
var fakeForm = $('<form style="display: none" action="' + action.attr('href') + '" method="' + formMethod + '">' + csrfToken + '</form>');
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);

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends "admin/change_list.html" %}{% load i18n %}
{% block search %}
<div class="language-selector" style="width: fit-content; margin-bottom: 1em;">
<label>{% trans "Language" %}:&nbsp;</label>
<select class="language-selector js-language-selector" style="min-width: 180px;">
{% for code, lang in cl.model_admin.get_language_tuple %}
<option value="{{ code }}"{% if cl.model_admin.language == code %} selected{% endif %}>{{ lang }}</option>
{% endfor %}
</select>
</div>{{ block.super }}
{% endblock %}
3 changes: 2 additions & 1 deletion djangocms_version_locking/test_utils/test_helpers.py
Original file line number Diff line number Diff line change
@@ -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


21 changes: 6 additions & 15 deletions djangocms_version_locking/utils.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 0 additions & 5 deletions docs/notifications.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
Notifications
==========================


Email notifications
------------------------
Configure email notifications to fail silently by setting: ``EMAIL_NOTIFICATIONS_FAIL_SILENTLY=True``
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@


INSTALL_REQUIREMENTS = [
'Django>=3.2,<5.0',
'Django>=4.0,<5.0',
'django-cms',
]

2 changes: 2 additions & 0 deletions test_settings.py
Original file line number Diff line number Diff line change
@@ -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,
}


9 changes: 0 additions & 9 deletions tests/requirements/dj32_cms40.txt

This file was deleted.

8 changes: 0 additions & 8 deletions tests/requirements/dj42_cms40.txt

This file was deleted.

9 changes: 9 additions & 0 deletions tests/requirements/dj42_cms41.txt
Original file line number Diff line number Diff line change
@@ -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
4 changes: 1 addition & 3 deletions tests/requirements/requirements_base.txt
Original file line number Diff line number Diff line change
@@ -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
pyflakes>=2.1.1
207 changes: 0 additions & 207 deletions tests/test_admin.py

This file was deleted.

292 changes: 0 additions & 292 deletions tests/test_admin_monkey_patch.py

This file was deleted.

53 changes: 0 additions & 53 deletions tests/test_check.py

This file was deleted.

129 changes: 0 additions & 129 deletions tests/test_emails.py

This file was deleted.

111 changes: 0 additions & 111 deletions tests/test_models.py

This file was deleted.

102 changes: 0 additions & 102 deletions tests/test_models_monkeypatch.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/test_toolbar_monkeypatch.py
Original file line number Diff line number Diff line change
@@ -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):
11 changes: 5 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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