richie 2.33.1.dev6__py2.py3-none-any.whl → 2.33.1.dev7__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of richie might be problematic. Click here for more details.

@@ -208,6 +208,11 @@
208
208
  @include sv-flex(0, 0, auto);
209
209
  }
210
210
 
211
+ // Define color variable for default item color on hover then available variants
212
+ @if r-theme-val(topbar, item-hover-color) {
213
+ --r--menu--item--hover--color: #{r-theme-val(topbar, item-hover-color)};
214
+ }
215
+
211
216
  & > a {
212
217
  @include sv-flex(1, 0, 100%);
213
218
  display: flex;
@@ -228,6 +233,7 @@
228
233
  @include media-breakpoint-up($r-topbar-breakpoint) {
229
234
  position: relative;
230
235
 
236
+ // If there is no default hover color we assume there is also no variant
231
237
  @if r-theme-val(topbar, item-hover-color) {
232
238
  &::after {
233
239
  content: '';
@@ -236,7 +242,7 @@
236
242
  left: 0;
237
243
  right: 0;
238
244
  height: 8px;
239
- background-color: r-theme-val(topbar, item-hover-color);
245
+ background-color: var(--r--menu--item--hover--color);
240
246
  border-top-left-radius: 0.2rem;
241
247
  border-top-right-radius: 0.2rem;
242
248
  }
@@ -1,19 +1,23 @@
1
- {% load cms_tags %}{% spaceless %}
1
+ {% load menu_tags %}{% spaceless %}
2
2
 
3
3
  {% for child in children %}
4
- {% with children_slug=child.get_menu_title|slugify %}
5
- <li class="topbar__item
4
+ {% with children_slug=child.get_menu_title|slugify menu_options=child.menu_extension %}
5
+ <li class="topbar__item dropdown
6
6
  {% if child.selected %} topbar__item--selected{% endif %}
7
7
  {% if child.ancestor %} topbar__item--ancestor{% endif %}
8
8
  {% if child.sibling %} topbar__item--sibling{% endif %}
9
- {% if child.descendant %} topbar__item--descendant{% endif %}">
10
- <a href="{{ child.attr.redirect_url|default:child.get_absolute_url }}">{{ child.get_menu_title }}</a>
11
- {% comment %}Dropdown menu are not yet implemented since there is no need about it for now{% endcomment %}
12
- {% comment %}{% if child.children %}
13
- <ul>
14
- {% show_menu from_level to_level extra_inactive extra_active template "" "" child %}
15
- </ul>
16
- {% endif %}{% endcomment %}
9
+ {% if child.descendant %} topbar__item--descendant{% endif %}
10
+ {% if menu_options.menu_color %} topbar__item--{{ menu_options.menu_color }}{% endif %}">
11
+ <a href="{{ child.attr.redirect_url|default:child.get_absolute_url }}">
12
+ {{ child.get_menu_title }}
13
+ </a>
14
+ {% comment %}Dropdown menu for children are only for page with index page
15
+ extension with a specific option enabled{% endcomment %}
16
+ {% if menu_options.allow_submenu and child.children %}
17
+ <ul>
18
+ {% show_menu from_level to_level extra_inactive extra_active template "" "" child %}
19
+ </ul>
20
+ {% endif %}
17
21
  </li>
18
22
  {% endwith %}
19
23
  {% endfor %}
@@ -135,7 +135,7 @@
135
135
 
136
136
  <nav class="topbar__menu">
137
137
  <ul class="topbar__list">
138
- {% show_menu 0 100 0 0 "menu/header_menu.html" %}
138
+ {% show_menu 0 100 100 100 "menu/header_menu.html" %}
139
139
  </ul>
140
140
  </nav>
141
141
  </div>
@@ -280,6 +280,21 @@ class CourseAdmin(FrontendEditableAdminMixin, PageExtensionAdmin):
280
280
  return JsonResponse({"id": new_page.course.id})
281
281
 
282
282
 
283
+ class MainMenuEntryAdmin(PageExtensionAdmin):
284
+ """
285
+ Admin class for the MainMenuEntry model
286
+ """
287
+
288
+ list_display = ["title", "allow_submenu"]
289
+
290
+ # pylint: disable=no-self-use
291
+ def title(self, obj):
292
+ """
293
+ Get the page title from the related page
294
+ """
295
+ return obj.extended_object.get_title()
296
+
297
+
283
298
  class OrganizationAdmin(PageExtensionAdmin):
284
299
  """
285
300
  Admin class for the Organization model
@@ -349,6 +364,7 @@ admin.site.register(models.Category, CategoryAdmin)
349
364
  admin.site.register(models.Course, CourseAdmin)
350
365
  admin.site.register(models.CourseRun, CourseRunAdmin)
351
366
  admin.site.register(models.Licence, LicenceAdmin)
367
+ admin.site.register(models.MainMenuEntry, MainMenuEntryAdmin)
352
368
  admin.site.register(models.Organization, OrganizationAdmin)
353
369
  admin.site.register(models.PageRole, PageRoleAdmin)
354
370
  admin.site.register(models.Person, PersonAdmin)
@@ -0,0 +1,79 @@
1
+ """
2
+ Menu modifier to add feature for menu template context.
3
+ """
4
+
5
+ from django.conf import settings
6
+
7
+ from menus.base import Modifier
8
+ from menus.menu_pool import menu_pool
9
+
10
+ from .models import MainMenuEntry
11
+
12
+
13
+ class MenuWithMainMenuEntry(Modifier):
14
+ """
15
+ Menu modifier to include MainMenuEntry extension data in menu template context.
16
+
17
+ In menu template you will be able to reach possible extension data from node
18
+ attribute ``menu_extension``. If node page has no extension it will have an empty
19
+ dict. Only a specific node level is processed and nodes with a different level
20
+ won't have the attribute ``menu_extension`` at all.
21
+ """
22
+
23
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
24
+ def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
25
+ """
26
+ Patch navigation nodes to include data from possible extension
27
+ ``MainMenuEntry``.
28
+
29
+ For performance:
30
+
31
+ * This does not work for breadcrumb navigation (all extension options are mean
32
+ for menu only);
33
+ * This works only on the menu top level, it means the one defined as first
34
+ argument from tag ``{% show_menu .. %}``;
35
+
36
+ Then to avoid making a query for each node item to retrieve its possible
37
+ extension object, we get the extensions in bulk as values instead of objects.
38
+
39
+ Finally we add the data on nodes so they can used from menu template.
40
+ """
41
+ # We are not altering breadcrumb menu, this is only for navigation menu and
42
+ # only for the visible menu (not the whole processed tree)
43
+ if not nodes or breadcrumb or not post_cut:
44
+ return nodes
45
+
46
+ # Get the page ids to process, only for the allowed node level
47
+ page_ids = [
48
+ node.id
49
+ for node in nodes
50
+ if node.level == settings.RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL
51
+ ]
52
+
53
+ # No need to continue if we don't have any valid node
54
+ if not page_ids:
55
+ return nodes
56
+
57
+ # We directly get the extensions from their related page id and serialized
58
+ # as a dict instead of model object
59
+ extension_queryset = MainMenuEntry.objects.filter(
60
+ extended_object_id__in=page_ids
61
+ ).values("extended_object_id", "allow_submenu", "menu_color")
62
+
63
+ # Pack extensions data into proper structure
64
+ extension_datas = {
65
+ item["extended_object_id"]: {
66
+ "allow_submenu": item["allow_submenu"],
67
+ "menu_color": item["menu_color"],
68
+ }
69
+ for item in extension_queryset
70
+ }
71
+
72
+ # Attach each possible extension data to its relative node
73
+ for node in nodes:
74
+ node.menu_extension = extension_datas.get(node.id, {})
75
+
76
+ return nodes
77
+
78
+
79
+ menu_pool.register_modifier(MenuWithMainMenuEntry)
@@ -2,6 +2,7 @@
2
2
  Toolbar extension for the courses application
3
3
  """
4
4
 
5
+ from django.conf import settings
5
6
  from django.utils.text import capfirst
6
7
  from django.utils.translation import gettext_lazy as _
7
8
 
@@ -12,7 +13,7 @@ from cms.utils.page_permissions import user_can_add_subpage, user_can_change_pag
12
13
  from cms.utils.urlutils import admin_reverse
13
14
 
14
15
  from .defaults import PAGE_EXTENSION_TOOLBAR_ITEM_POSITION
15
- from .models import Category, Course, Organization, Person
16
+ from .models import Category, Course, MainMenuEntry, Organization, Person
16
17
 
17
18
 
18
19
  class BaseExtensionToolbar(ExtensionToolbar):
@@ -131,3 +132,52 @@ class PersonExtensionToolbar(BaseExtensionToolbar):
131
132
  """
132
133
 
133
134
  model = Person
135
+
136
+
137
+ @toolbar_pool.register
138
+ class MainMenuEntryExtensionToolbar(BaseExtensionToolbar):
139
+ """
140
+ This extension class customizes the toolbar for the MainMenuEntry page extension.
141
+ """
142
+
143
+ model = MainMenuEntry
144
+
145
+ def populate(self):
146
+ """
147
+ Specific extension populate method.
148
+
149
+ This extension entry only appears in toolbar if page already have extension or
150
+ if setting ``RICHIE_MAINMENUENTRY_ALLOW_CREATION`` is true. Finally the page
151
+ level must also match the allowed level from setting
152
+ ``RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL``.
153
+ """
154
+ # always use draft if we have a page
155
+ self.page = get_page_draft(self.request.current_page)
156
+ if not self.page:
157
+ # Nothing to do
158
+ return
159
+
160
+ # setup the extension toolbar with permissions and sanity checks
161
+ page_menu = self._setup_extension_toolbar()
162
+
163
+ if user_can_change_page(user=self.request.user, page=self.page):
164
+ # Retrieves extension instance (if any) and toolbar URL
165
+ page_extension, admin_url = self.get_page_extension_admin()
166
+ # Get the page node level
167
+ level = self.page.node.get_depth() - 1
168
+ allowed = page_extension is not None or (
169
+ page_extension is None
170
+ and settings.RICHIE_MAINMENUENTRY_ALLOW_CREATION is True
171
+ )
172
+ if (
173
+ allowed
174
+ and level == settings.RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL
175
+ and admin_url
176
+ ):
177
+ # Adds a toolbar item in position 0 (at the top of the menu)
178
+ page_menu.add_modal_item(
179
+ _("Main menu settings"),
180
+ url=admin_url,
181
+ disabled=not self.toolbar.edit_mode_active,
182
+ position=PAGE_EXTENSION_TOOLBAR_ITEM_POSITION,
183
+ )
@@ -312,6 +312,10 @@ ORGANIZATIONS_PAGE = {
312
312
  "reverse_id": "organizations",
313
313
  "template": "courses/cms/organization_detail.html",
314
314
  }
315
+ MENUENTRIES_PAGE = {
316
+ "reverse_id": None,
317
+ "template": "richie/single_column.html",
318
+ }
315
319
  PERSONS_PAGE = {"reverse_id": "persons", "template": "courses/cms/person_detail.html"}
316
320
  PROGRAMS_PAGE = {
317
321
  "reverse_id": "programs",
@@ -382,3 +386,11 @@ EFFORT_UNITS = {
382
386
  # Maximum number of archived course runs displayed by default on course detail page.
383
387
  # The additional runs can be viewed by clicking on `View more` link.
384
388
  RICHIE_MAX_ARCHIVED_COURSE_RUNS = 10
389
+
390
+ # Define possible hover color that can be choosen for an MainMenuEntry and to apply on
391
+ # its menu item
392
+ MENU_ENTRY_COLOR_CLASSES = getattr(
393
+ settings,
394
+ "RICHIE_MENU_ENTRY_COLOR_CLASSES",
395
+ (("", _("None")),),
396
+ )
@@ -910,3 +910,26 @@ class ProgramFactory(PageExtensionDjangoModelFactory):
910
910
  plugin_type="PlainTextPlugin",
911
911
  body=text,
912
912
  )
913
+
914
+
915
+ class MainMenuEntryFactory(BLDPageExtensionDjangoModelFactory):
916
+ """
917
+ A factory to automatically generate random yet meaningful menu entry page extensions
918
+ and their related page in our tests.
919
+ """
920
+
921
+ class Meta:
922
+ model = models.MainMenuEntry
923
+ exclude = [
924
+ "page_in_navigation",
925
+ "page_languages",
926
+ "page_parent",
927
+ "page_reverse_id",
928
+ "page_template",
929
+ "page_title",
930
+ ]
931
+
932
+ # fields concerning the related page
933
+ page_template = models.MainMenuEntry.PAGE["template"]
934
+ allow_submenu = False
935
+ menu_color = ""
@@ -0,0 +1,74 @@
1
+ # Generated by Django 4.2.16 on 2024-09-19 00:24
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+ from ..defaults import MENU_ENTRY_COLOR_CLASSES
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("cms", "0022_auto_20180620_1551"),
13
+ ("courses", "0034_auto_20230817_1736"),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="MainMenuEntry",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.AutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ (
30
+ "allow_submenu",
31
+ models.BooleanField(
32
+ default=False,
33
+ help_text="If enabled the page entry in menu will be a dropdown for its possible children.",
34
+ verbose_name="Allow submenu",
35
+ ),
36
+ ),
37
+ (
38
+ "menu_color",
39
+ models.CharField(
40
+ blank=True,
41
+ choices=MENU_ENTRY_COLOR_CLASSES,
42
+ default="",
43
+ help_text="A color used to display page entry in menu.",
44
+ max_length=10,
45
+ verbose_name="Color in menu",
46
+ ),
47
+ ),
48
+ (
49
+ "extended_object",
50
+ models.OneToOneField(
51
+ editable=False,
52
+ on_delete=django.db.models.deletion.CASCADE,
53
+ to="cms.page",
54
+ ),
55
+ ),
56
+ (
57
+ "public_extension",
58
+ models.OneToOneField(
59
+ editable=False,
60
+ null=True,
61
+ on_delete=django.db.models.deletion.CASCADE,
62
+ related_name="draft_extension",
63
+ to="courses.mainmenuentry",
64
+ ),
65
+ ),
66
+ ],
67
+ options={
68
+ "verbose_name": "main menu entry",
69
+ "verbose_name_plural": "main menu entries",
70
+ "db_table": "richie_menuentry",
71
+ "ordering": ["-pk"],
72
+ },
73
+ ),
74
+ ]
@@ -6,6 +6,7 @@
6
6
  from .blog import *
7
7
  from .category import *
8
8
  from .course import *
9
+ from .menuentry import *
9
10
  from .organization import *
10
11
  from .person import *
11
12
  from .program import *
@@ -0,0 +1,51 @@
1
+ """
2
+ Declare and configure the models for the menu entry part
3
+ """
4
+
5
+ from django.db import models
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+ from cms.extensions.extension_pool import extension_pool
9
+
10
+ from ...core.models import BasePageExtension
11
+ from .. import defaults
12
+
13
+
14
+ class MainMenuEntry(BasePageExtension):
15
+ """
16
+ The MainMenuEntry extension defines some options for a page entry in the main menu.
17
+ """
18
+
19
+ PAGE = defaults.MENUENTRIES_PAGE
20
+
21
+ class Meta:
22
+ db_table = "richie_menuentry"
23
+ ordering = ["-pk"]
24
+ verbose_name = _("main menu entry")
25
+ verbose_name_plural = _("main menu entries")
26
+
27
+ def __str__(self):
28
+ """Human representation of an main menu entry page"""
29
+ model = self._meta.verbose_name.title()
30
+ name = self.extended_object.get_title()
31
+ return f"{model:s}: {name:s}"
32
+
33
+ allow_submenu = models.BooleanField(
34
+ _("Allow submenu"),
35
+ default=False,
36
+ help_text=_(
37
+ "If enabled the page entry in menu will be a dropdown for its possible "
38
+ "children."
39
+ ),
40
+ )
41
+ menu_color = models.CharField(
42
+ _("Color in menu"),
43
+ max_length=10,
44
+ default="",
45
+ blank=True,
46
+ choices=defaults.MENU_ENTRY_COLOR_CLASSES,
47
+ help_text=_("A color used to display page entry in menu."),
48
+ )
49
+
50
+
51
+ extension_pool.register(MainMenuEntry)
@@ -597,3 +597,14 @@ RICHIE_SIMPLEPICTURE_PRESETS = {
597
597
  "sizes": "60px",
598
598
  },
599
599
  }
600
+
601
+ # If true the toolbar item will already be showed. If false only a page which already
602
+ # have the extension will have the toolbar item and users won't be able to add
603
+ # MainMenuEntry extension on existing page, only create new page with index extension
604
+ # through the wizard.
605
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION = False
606
+
607
+ # Define which node level can be processed to search for MainMenuEntry extension. You
608
+ # can set it to 'None' for never processing any node.
609
+ # This is a limit against performance issues to avoid making querysets for nothing.
610
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL = 0