1+ import json
12from collections import OrderedDict
23from urllib .parse import urlparse
34
89from django .contrib .contenttypes .models import ContentType
910from django .core .exceptions import ImproperlyConfigured , ObjectDoesNotExist
1011from django .db .models .functions import Lower
12+ from django .forms import MediaDefiningClass
1113from django .http import Http404 , HttpResponseNotAllowed
1214from django .shortcuts import redirect , render
1315from django .template .loader import render_to_string , select_template
2426
2527from . import versionables
2628from .conf import USERNAME_FIELD
27- from .constants import DRAFT , PUBLISHED
29+ from .constants import DRAFT , INDICATOR_DESCRIPTIONS , PUBLISHED
2830from .exceptions import ConditionFailed
2931from .forms import grouper_form_factory
3032from .helpers import (
3133 get_admin_url ,
3234 get_editable_url ,
35+ get_latest_admin_viewable_content ,
3336 get_preview_url ,
3437 proxy_model ,
3538 version_list_url ,
3639)
40+ from .indicators import content_indicator , content_indicator_menu
3741from .models import Version
3842from .versionables import _cms_extension
3943
@@ -42,16 +46,24 @@ class VersioningChangeListMixin:
4246 """Mixin used for ChangeList classes of content models."""
4347
4448 def get_queryset (self , request ):
45- """Limit the content model queryset to latest versions only."""
49+ """Limit the content model queryset to the latest versions only."""
4650 queryset = super ().get_queryset (request )
4751 versionable = versionables .for_content (queryset .model )
4852
49- # TODO: Improve the grouping filters to use anything defined in the
50- # apps versioning config extra_grouping_fields
51- grouping_filters = {}
52- if 'language' in versionable .extra_grouping_fields :
53- grouping_filters ['language' ] = get_language_from_request (request )
53+ """Check if there is a method "self.get_<field>_from_request" for each extra grouping field.
54+ If so call it to retrieve the appropriate filter. If no method is found (except for "language")
55+ no filter is applied. For "language" the fallback is versioning's "get_language_frmo_request".
56+
57+ Admins requiring extra grouping field beside "language" need to implement the "get_<field>_from_request"
58+ method themselves. A common way to select the field might be GET or POST parameters or user-related settings.
59+ """
5460
61+ grouping_filters = {}
62+ for field in versionable .extra_grouping_fields :
63+ if hasattr (self .model_admin , f"get_{ field } _from_request" ):
64+ grouping_filters [field ] = getattr (self .model_admin , f"get_{ field } _from_request" )(request )
65+ elif field == "language" :
66+ grouping_filters [field ] = get_language_from_request (request )
5567 return queryset .filter (pk__in = versionable .distinct_groupers (** grouping_filters ))
5668
5769
@@ -116,7 +128,75 @@ def has_change_permission(self, request, obj=None):
116128 return super ().has_change_permission (request , obj )
117129
118130
119- class ExtendedVersionAdminMixin (VersioningAdminMixin ):
131+ class StateIndicatorMixin (metaclass = MediaDefiningClass ):
132+ """Mixin to provide state_indicator column to the changelist view of a content model admin. Usage::
133+
134+ class MyContentModelAdmin(StateIndicatorMixin, admin.ModelAdmin):
135+ list_display = [..., "state_indicator", ...]
136+ """
137+ class Media :
138+ # js for the context menu
139+ js = ("admin/js/jquery.init.js" , "djangocms_versioning/js/indicators.js" ,)
140+ # css for indicators and context menu
141+ css = {
142+ "all" : (static_with_version ("cms/css/cms.pagetree.css" ),),
143+ }
144+
145+ indicator_column_label = _ ("State" )
146+
147+ @property
148+ def _extra_grouping_fields (self ):
149+ try :
150+ return versionables .for_grouper (self .model ).extra_grouping_fields
151+ except KeyError :
152+ return None
153+
154+ def get_indicator_column (self , request ):
155+ def indicator (obj ):
156+ if self ._extra_grouping_fields is not None : # Grouper Model
157+ content_obj = get_latest_admin_viewable_content (obj , include_unpublished_archived = True , ** {
158+ field : getattr (self , field ) for field in self ._extra_grouping_fields
159+ })
160+ else : # Content Model
161+ content_obj = obj
162+ status = content_indicator (content_obj )
163+ menu = content_indicator_menu (
164+ request ,
165+ status ,
166+ content_obj ._version ,
167+ back = request .path_info + "?" + request .GET .urlencode (),
168+ ) if status else None
169+ return render_to_string (
170+ "admin/djangocms_versioning/indicator.html" ,
171+ {
172+ "state" : status or "empty" ,
173+ "description" : INDICATOR_DESCRIPTIONS .get (status , _ ("Empty" )),
174+ "menu_template" : "admin/cms/page/tree/indicator_menu.html" ,
175+ "menu" : json .dumps (render_to_string ("admin/cms/page/tree/indicator_menu.html" ,
176+ dict (indicator_menu_items = menu ))) if menu else None ,
177+ }
178+ )
179+ indicator .short_description = self .indicator_column_label
180+ return indicator
181+
182+ def state_indicator (self , obj ):
183+ raise ValueError (
184+ "ModelAdmin.display_list contains \" state_indicator\" as a placeholder for status indicators. "
185+ "Status indicators, however, are not loaded. If you implement \" get_list_display\" make "
186+ "sure it calls super().get_list_display."
187+ ) # pragma: no cover
188+
189+ def get_list_display (self , request ):
190+ """Default behavior: replaces the text "state_indicator" by the indicator column"""
191+ if versionables .exists_for_content (self .model ) or versionables .exists_for_grouper (self .model ):
192+ return tuple (self .get_indicator_column (request ) if item == "state_indicator" else item
193+ for item in super ().get_list_display (request ))
194+ else :
195+ # remove "state_indicator" entry
196+ return tuple (item for item in super ().get_list_display (request ) if item != "state_indicator" )
197+
198+
199+ class ExtendedVersionAdminMixin (VersioningAdminMixin , metaclass = MediaDefiningClass ):
120200 """
121201 Extended VersionAdminMixin for common/generic versioning admin items
122202
@@ -125,6 +205,11 @@ class ExtendedVersionAdminMixin(VersioningAdminMixin):
125205 """
126206
127207 change_list_template = "djangocms_versioning/admin/mixin/change_list.html"
208+ versioning_list_display = (
209+ "get_author" ,
210+ "get_modified_date" ,
211+ "get_versioning_state" ,
212+ )
128213
129214 class Media :
130215 js = ("admin/js/jquery.init.js" , "djangocms_versioning/js/actions.js" )
@@ -269,11 +354,14 @@ def get_list_actions(self):
269354 """
270355 Collect rendered actions from implemented methods and return as list
271356 """
272- return [
357+ actions = [
273358 self ._get_preview_link ,
274359 self ._get_edit_link ,
275- self ._get_manage_versions_link ,
276- ]
360+ ]
361+ if "state_indicator" not in self .versioning_list_display :
362+ # State indicator mixin loaded?
363+ actions .append (self ._get_manage_versions_link )
364+ return actions
277365
278366 def get_preview_link (self , obj ):
279367 return format_html (
@@ -310,14 +398,9 @@ def extend_list_display(self, request, modifier_dict, list_display):
310398
311399 def get_list_display (self , request ):
312400 # get configured list_display
313- list_display = self . list_display
401+ list_display = super (). get_list_display ( request )
314402 # Add versioning information and action fields
315- list_display += (
316- "get_author" ,
317- "get_modified_date" ,
318- "get_versioning_state" ,
319- self ._list_actions (request )
320- )
403+ list_display += self .versioning_list_display + (self ._list_actions (request ),)
321404 # Get the versioning extension
322405 extension = _cms_extension ()
323406 modifier_dict = extension .add_to_field_extension .get (self .model , None )
@@ -326,6 +409,14 @@ def get_list_display(self, request):
326409 return list_display
327410
328411
412+ class ExtendedIndicatorVersionAdminMixin (StateIndicatorMixin , ExtendedVersionAdminMixin ):
413+ versioning_list_display = (
414+ "get_author" ,
415+ "get_modified_date" ,
416+ "state_indicator" ,
417+ )
418+
419+
329420class VersionChangeList (ChangeList ):
330421 def get_filters_params (self , params = None ):
331422 """Removes the grouper param from the filters as the main grouper
@@ -697,7 +788,7 @@ def archive_view(self, request, object_id):
697788 ),
698789 args = (version .content .pk ,),
699790 ),
700- back_url = version_list_url ( version . content ),
791+ back_url = self . back_link ( request , version ),
701792 )
702793 return render (
703794 request , "djangocms_versioning/admin/archive_confirmation.html" , context
@@ -777,7 +868,7 @@ def unpublish_view(self, request, object_id):
777868 ),
778869 args = (version .content .pk ,),
779870 ),
780- back_url = version_list_url ( version . content ),
871+ back_url = self . back_link ( request , version ),
781872 )
782873 extra_context = OrderedDict (
783874 [
@@ -891,7 +982,7 @@ def revert_view(self, request, object_id):
891982 ),
892983 args = (version .content .pk ,),
893984 ),
894- back_url = version_list_url ( version . content ),
985+ back_url = self . back_link ( request , version ),
895986 )
896987 return render (
897988 request , "djangocms_versioning/admin/revert_confirmation.html" , context
@@ -933,7 +1024,7 @@ def discard_view(self, request, object_id):
9331024 ),
9341025 args = (version .content .pk ,),
9351026 ),
936- back_url = version_list_url ( version . content ),
1027+ back_url = self . back_link ( request , version ),
9371028 )
9381029 return render (
9391030 request , "djangocms_versioning/admin/discard_confirmation.html" , context
@@ -969,14 +1060,6 @@ def compare_view(self, request, object_id):
9691060 ),
9701061 ** persist_params
9711062 )
972- return_url = request .GET .get ("back" , version_list_url (v1 .content ))
973- try :
974- # Is return url a valid url?
975- resolve (urlparse (return_url )[2 ])
976- except Resolver404 :
977- # If not ignore
978- return_url = None
979-
9801063 # Get the list of versions for the grouper. This is for use
9811064 # in the dropdown to choose a version.
9821065 version_list = Version .objects .filter_by_content_grouping_values (
@@ -987,7 +1070,7 @@ def compare_view(self, request, object_id):
9871070 "version_list" : version_list ,
9881071 "v1" : v1 ,
9891072 "v1_preview_url" : v1_preview_url ,
990- "return_url" : return_url ,
1073+ "return_url" : self . back_link ( request , v1 ) ,
9911074 }
9921075
9931076 # Now check if version 2 has been specified and add to context
@@ -1015,6 +1098,18 @@ def compare_view(self, request, object_id):
10151098 request , "djangocms_versioning/admin/compare.html" , context
10161099 )
10171100
1101+ @staticmethod
1102+ def back_link (request , version = None ):
1103+ back_url = request .GET .get ("back" , None )
1104+ if back_url :
1105+ try :
1106+ # Is return url a valid url?
1107+ resolve (urlparse (back_url )[2 ])
1108+ except Resolver404 :
1109+ # If not ignore
1110+ back_url = None
1111+ return back_url or (version_list_url (version .content ) if version else None )
1112+
10181113 def changelist_view (self , request , extra_context = None ):
10191114 """Handle grouper filtering on the changelist"""
10201115 if not request .GET :
0 commit comments