richie 2.34.1.dev10__py2.py3-none-any.whl → 2.34.1.dev16__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.
- frontend/scss/colors/_theme.scss +8 -0
- frontend/scss/components/_header.scss +103 -14
- frontend/scss/objects/_selector.scss +1 -0
- richie/apps/core/cache.py +175 -2
- richie/apps/core/storage.py +63 -0
- richie/apps/core/templates/menu/header_menu.html +31 -11
- richie/apps/core/templates/richie/base.html +149 -6
- richie/apps/core/tests/test_cache.py +159 -0
- richie/apps/core/tests/test_settings.py +52 -0
- richie/apps/core/utils.py +60 -0
- richie/apps/courses/migrations/0037_alter_blogpostpluginmodel_cmsplugin_ptr_and_more.py +230 -0
- richie/apps/courses/migrations/0038_alter_mainmenuentry_menu_color.py +25 -0
- richie/apps/courses/models/menuentry.py +1 -1
- richie/plugins/glimpse/migrations/0004_alter_glimpse_cmsplugin_ptr_alter_glimpse_variant.py +49 -0
- richie/plugins/html_sitemap/migrations/0002_alter_htmlsitemappage_cmsplugin_ptr.py +28 -0
- richie/plugins/large_banner/migrations/0004_alter_largebanner_cmsplugin_ptr.py +28 -0
- richie/plugins/lti_consumer/migrations/0004_alter_lticonsumer_cmsplugin_ptr.py +28 -0
- richie/plugins/nesteditem/migrations/0004_alter_nesteditem_cmsplugin_ptr.py +28 -0
- richie/plugins/plain_text/migrations/0002_alter_plaintext_cmsplugin_ptr.py +28 -0
- richie/static/richie/css/main.css +1 -1
- {richie-2.34.1.dev10.dist-info → richie-2.34.1.dev16.dist-info}/METADATA +3 -1
- {richie-2.34.1.dev10.dist-info → richie-2.34.1.dev16.dist-info}/RECORD +26 -14
- {richie-2.34.1.dev10.dist-info → richie-2.34.1.dev16.dist-info}/WHEEL +1 -1
- {richie-2.34.1.dev10.dist-info → richie-2.34.1.dev16.dist-info}/LICENSE +0 -0
- {richie-2.34.1.dev10.dist-info → richie-2.34.1.dev16.dist-info}/top_level.txt +0 -0
- {richie-2.34.1.dev10.dist-info → richie-2.34.1.dev16.dist-info}/zip-safe +0 -0
frontend/scss/colors/_theme.scss
CHANGED
|
@@ -47,6 +47,14 @@ $r-theme: (
|
|
|
47
47
|
item-cta-hollow-background: transparent,
|
|
48
48
|
item-cta-hollow-border: transparent,
|
|
49
49
|
item-divider-border: r-color('light-grey'),
|
|
50
|
+
dropdown-border-radius: 8px,
|
|
51
|
+
dropdown-padding: 0.5rem,
|
|
52
|
+
dropdown-gap: 0.5rem,
|
|
53
|
+
dropdown-background-color: r-color('azure2'),
|
|
54
|
+
dropdown-item-background-color: r-color('indianred3'),
|
|
55
|
+
dropdown-item-text-color: r-color('white'),
|
|
56
|
+
dropdown-item-border-radius: 4px,
|
|
57
|
+
dropdown-item-padding: 0.5rem 1rem,
|
|
50
58
|
),
|
|
51
59
|
body-content: (
|
|
52
60
|
base-color: r-color('black'),
|
|
@@ -168,6 +168,10 @@
|
|
|
168
168
|
flex-wrap: nowrap;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
& > .topbar__list {
|
|
172
|
+
position: relative;
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
// Aside menu variation
|
|
172
176
|
&--aside {
|
|
173
177
|
@include sv-flex(1, 0, auto);
|
|
@@ -197,6 +201,76 @@
|
|
|
197
201
|
// Menu item element
|
|
198
202
|
&__item {
|
|
199
203
|
$item-selector: &;
|
|
204
|
+
&.dropdown {
|
|
205
|
+
display: block;
|
|
206
|
+
position: static;
|
|
207
|
+
|
|
208
|
+
& > button {
|
|
209
|
+
width: 100%;
|
|
210
|
+
|
|
211
|
+
& > .icon {
|
|
212
|
+
height: 1rem;
|
|
213
|
+
margin-left: 0.5rem;
|
|
214
|
+
width: 1rem;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
& ul[role='menu'] {
|
|
219
|
+
padding-left: 0.5rem;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@include media-breakpoint-up($r-topbar-breakpoint) {
|
|
224
|
+
&.dropdown {
|
|
225
|
+
// See header_menu.html for the definition of the variables
|
|
226
|
+
--active-background-color: #{r-theme-val(topbar, dropdown-item-background-color)};
|
|
227
|
+
--active-text-color: #{r-theme-val(topbar, dropdown-item-text-color)};
|
|
228
|
+
|
|
229
|
+
& > button[aria-expanded='true'] {
|
|
230
|
+
background-color: r-theme-val(topbar, dropdown-background-color);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
& > button[aria-expanded='true'] + .topbar__sublist {
|
|
234
|
+
display: block;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
& > .topbar__sublist {
|
|
238
|
+
background: r-theme-val(topbar, dropdown-background-color);
|
|
239
|
+
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
|
240
|
+
border-radius: r-theme-val(topbar, dropdown-border-radius);
|
|
241
|
+
display: none;
|
|
242
|
+
position: absolute;
|
|
243
|
+
transform: translateX(-8px);
|
|
244
|
+
max-width: 100%;
|
|
245
|
+
|
|
246
|
+
&--full-width {
|
|
247
|
+
left: 0;
|
|
248
|
+
transform: inherit;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
& > ul[role='menu'] {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-wrap: wrap;
|
|
254
|
+
gap: r-theme-val(topbar, dropdown-gap);
|
|
255
|
+
padding: r-theme-val(topbar, dropdown-padding);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
& .topbar__item > a {
|
|
259
|
+
background-color: #fff;
|
|
260
|
+
border-radius: r-theme-val(topbar, dropdown-item-border-radius);
|
|
261
|
+
padding: r-theme-val(topbar, dropdown-item-padding);
|
|
262
|
+
&::after {
|
|
263
|
+
display: none;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
&:is(:focus, :hover) {
|
|
267
|
+
background-color: var(--active-background-color);
|
|
268
|
+
color: var(--active-text-color);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
200
274
|
|
|
201
275
|
@include sv-flex(1, 0, auto);
|
|
202
276
|
display: flex;
|
|
@@ -213,15 +287,23 @@
|
|
|
213
287
|
--r--menu--item--hover--color: #{r-theme-val(topbar, item-hover-color)};
|
|
214
288
|
}
|
|
215
289
|
|
|
216
|
-
& >
|
|
290
|
+
& > button {
|
|
291
|
+
@include button-reset-style();
|
|
292
|
+
--radius: #{r-theme-val(topbar, dropdown-border-radius)};
|
|
293
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
& > a,
|
|
297
|
+
& > button {
|
|
217
298
|
@include sv-flex(1, 0, 100%);
|
|
299
|
+
align-items: center;
|
|
300
|
+
color: inherit;
|
|
218
301
|
display: flex;
|
|
219
|
-
padding: 1rem 0.2rem 1rem 1rem;
|
|
220
302
|
flex-direction: row;
|
|
221
|
-
align-items: center;
|
|
222
303
|
font-family: inherit;
|
|
223
304
|
font-weight: inherit;
|
|
224
|
-
|
|
305
|
+
justify-content: space-between;
|
|
306
|
+
padding: 1rem 0.2rem 1rem 1rem;
|
|
225
307
|
|
|
226
308
|
@include media-breakpoint-up($r-topbar-breakpoint) {
|
|
227
309
|
padding: 1rem 1rem;
|
|
@@ -235,18 +317,22 @@
|
|
|
235
317
|
|
|
236
318
|
// If there is no default hover color we assume there is also no variant
|
|
237
319
|
@if r-theme-val(topbar, item-hover-color) {
|
|
238
|
-
|
|
239
|
-
content: '';
|
|
240
|
-
position: absolute;
|
|
241
|
-
bottom: 0;
|
|
242
|
-
left: 0;
|
|
243
|
-
right: 0;
|
|
244
|
-
height: 8px;
|
|
320
|
+
&:is(a)::after {
|
|
245
321
|
background-color: var(--r--menu--item--hover--color);
|
|
246
322
|
border-top-left-radius: 0.2rem;
|
|
247
323
|
border-top-right-radius: 0.2rem;
|
|
324
|
+
bottom: 0;
|
|
325
|
+
content: '';
|
|
326
|
+
height: 8px;
|
|
327
|
+
left: 0;
|
|
328
|
+
position: absolute;
|
|
329
|
+
right: 0;
|
|
248
330
|
}
|
|
249
331
|
}
|
|
332
|
+
|
|
333
|
+
&:is(button) {
|
|
334
|
+
background-color: r-theme-val(topbar, dropdown-background-color);
|
|
335
|
+
}
|
|
250
336
|
}
|
|
251
337
|
}
|
|
252
338
|
}
|
|
@@ -257,8 +343,10 @@
|
|
|
257
343
|
|
|
258
344
|
// Current page item or current ancestor
|
|
259
345
|
&--selected,
|
|
260
|
-
&--ancestor
|
|
261
|
-
|
|
346
|
+
&--ancestor,
|
|
347
|
+
&:has(.topbar__sublist .topbar__item.topbar__item--selected) {
|
|
348
|
+
& > a,
|
|
349
|
+
& > button {
|
|
262
350
|
position: relative;
|
|
263
351
|
color: r-theme-val(topbar, item-active-color);
|
|
264
352
|
|
|
@@ -328,7 +416,8 @@
|
|
|
328
416
|
}
|
|
329
417
|
|
|
330
418
|
// Item divider
|
|
331
|
-
& + #{$item-selector}
|
|
419
|
+
& + #{$item-selector},
|
|
420
|
+
& > .topbar__list #{$item-selector} {
|
|
332
421
|
@if r-theme-val(topbar, item-divider-border) {
|
|
333
422
|
border-top: $onepixel solid r-theme-val(topbar, item-divider-border);
|
|
334
423
|
}
|
richie/apps/core/cache.py
CHANGED
|
@@ -3,22 +3,34 @@ Plugin for django-redis that supports redis sentinel.
|
|
|
3
3
|
Credits :
|
|
4
4
|
- https://github.com/lamoda/django-sentinel
|
|
5
5
|
- https://github.com/KabbageInc/django-redis-sentinel
|
|
6
|
+
|
|
7
|
+
RedisCache with a fallback cache to prevent denial of service if Redis is down
|
|
8
|
+
Freely inspired by django-cache-fallback
|
|
9
|
+
|
|
10
|
+
Credits:
|
|
11
|
+
- https://github.com/Kub-AT/django-cache-fallback/
|
|
6
12
|
"""
|
|
7
13
|
|
|
8
14
|
import logging
|
|
9
15
|
import random
|
|
10
16
|
|
|
11
17
|
from django.conf import settings
|
|
18
|
+
from django.core.cache import caches
|
|
19
|
+
from django.core.cache.backends.base import BaseCache
|
|
12
20
|
from django.core.exceptions import ImproperlyConfigured
|
|
13
21
|
from django.utils.cache import get_max_age, patch_response_headers
|
|
14
22
|
from django.utils.deprecation import MiddlewareMixin
|
|
15
23
|
|
|
24
|
+
from cms.cache import CMS_PAGE_CACHE_VERSION_KEY
|
|
25
|
+
from django_redis.cache import RedisCache
|
|
16
26
|
from django_redis.client import DefaultClient
|
|
17
27
|
from redis.sentinel import Sentinel
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
from .utils import Throttle
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
FALLBACK_CACHE_INVALIDATION_INTERVAL = 60 # seconds
|
|
32
|
+
DJANGO_REDIS_LOGGER = getattr(settings, "DJANGO_REDIS_LOGGER", __name__)
|
|
33
|
+
logger = logging.getLogger(DJANGO_REDIS_LOGGER)
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
class SentinelClient(DefaultClient):
|
|
@@ -174,3 +186,164 @@ class LimitBrowserCacheTTLHeaders(MiddlewareMixin):
|
|
|
174
186
|
patch_response_headers(response, cache_timeout=max_ttl)
|
|
175
187
|
|
|
176
188
|
return response
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class RedisCacheWithFallback(BaseCache):
|
|
192
|
+
"""
|
|
193
|
+
BaseCache object with a redis_cache used as main cache
|
|
194
|
+
and the "fallback" aliased cache which takes over
|
|
195
|
+
in case redis_cache is down.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, server, params):
|
|
199
|
+
"""
|
|
200
|
+
Instantiate the Redis Cache with server and params
|
|
201
|
+
and retrieve the cache with alias "fallback"
|
|
202
|
+
"""
|
|
203
|
+
super().__init__(params)
|
|
204
|
+
self.redis_cache = RedisCache(server, params)
|
|
205
|
+
self.fallback_cache = caches["memory_cache"]
|
|
206
|
+
|
|
207
|
+
def _call_with_fallback(self, method, *args, **kwargs):
|
|
208
|
+
"""
|
|
209
|
+
Try first to exec provided method through Redis cache instance,
|
|
210
|
+
in case of success, invalidate the fallback cache so it is clean and
|
|
211
|
+
ready for next failure,
|
|
212
|
+
in case of failure, logger reports the exception and
|
|
213
|
+
the fallback cache takes over.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
next_cache_state = self._call_redis_cache(method, args, kwargs)
|
|
217
|
+
self._invalidate_fallback_cache()
|
|
218
|
+
return next_cache_state
|
|
219
|
+
except Exception as e: # pylint: disable=W0718
|
|
220
|
+
logger.warning("[DEGRADED CACHE MODE] - Switch to fallback cache")
|
|
221
|
+
logger.exception(e)
|
|
222
|
+
return self._call_fallback_cache(method, args, kwargs)
|
|
223
|
+
|
|
224
|
+
def _call_redis_cache(self, method, args, kwargs):
|
|
225
|
+
"""
|
|
226
|
+
Exec the provided method through the redis cache instance
|
|
227
|
+
"""
|
|
228
|
+
return getattr(self.redis_cache, method)(*args, **kwargs)
|
|
229
|
+
|
|
230
|
+
def _call_fallback_cache(self, method, args, kwargs):
|
|
231
|
+
"""
|
|
232
|
+
Exec the provided method through the fallback cache instance
|
|
233
|
+
"""
|
|
234
|
+
return getattr(self.fallback_cache, method)(*args, **kwargs)
|
|
235
|
+
|
|
236
|
+
@Throttle(FALLBACK_CACHE_INVALIDATION_INTERVAL) # 60 seconds
|
|
237
|
+
def _invalidate_fallback_cache(self):
|
|
238
|
+
"""
|
|
239
|
+
Invalidate cms page cache in the fallback cache.
|
|
240
|
+
"""
|
|
241
|
+
self.fallback_cache.delete(CMS_PAGE_CACHE_VERSION_KEY)
|
|
242
|
+
|
|
243
|
+
def get_backend_timeout(self, *args, **kwargs):
|
|
244
|
+
"""
|
|
245
|
+
Pass get_backend_timeout cache method to _call_with_fallback
|
|
246
|
+
"""
|
|
247
|
+
return self._call_with_fallback("get_backend_timeout", *args, **kwargs)
|
|
248
|
+
|
|
249
|
+
def make_key(self, *args, **kwargs):
|
|
250
|
+
"""
|
|
251
|
+
Pass make_key cache method to _call_with_fallback
|
|
252
|
+
"""
|
|
253
|
+
return self._call_with_fallback("make_key", *args, **kwargs)
|
|
254
|
+
|
|
255
|
+
def add(self, *args, **kwargs):
|
|
256
|
+
"""
|
|
257
|
+
Pass add cache method to _call_with_fallback
|
|
258
|
+
"""
|
|
259
|
+
return self._call_with_fallback("add", *args, **kwargs)
|
|
260
|
+
|
|
261
|
+
def get(self, *args, **kwargs):
|
|
262
|
+
"""
|
|
263
|
+
Pass get cache method to _call_with_fallback
|
|
264
|
+
"""
|
|
265
|
+
return self._call_with_fallback("get", *args, **kwargs)
|
|
266
|
+
|
|
267
|
+
def set(self, *args, **kwargs):
|
|
268
|
+
"""
|
|
269
|
+
Pass set cache method to _call_with_fallback
|
|
270
|
+
"""
|
|
271
|
+
return self._call_with_fallback("set", *args, **kwargs)
|
|
272
|
+
|
|
273
|
+
def touch(self, *args, **kwargs):
|
|
274
|
+
"""
|
|
275
|
+
Pass touch cache method to _call_with_fallback
|
|
276
|
+
"""
|
|
277
|
+
return self._call_with_fallback("touch", *args, **kwargs)
|
|
278
|
+
|
|
279
|
+
def delete(self, *args, **kwargs):
|
|
280
|
+
"""
|
|
281
|
+
Pass delete cache method to _call_with_fallback
|
|
282
|
+
"""
|
|
283
|
+
return self._call_with_fallback("delete", *args, **kwargs)
|
|
284
|
+
|
|
285
|
+
def get_many(self, *args, **kwargs):
|
|
286
|
+
"""
|
|
287
|
+
Pass get_many cache method to _call_with_fallback
|
|
288
|
+
"""
|
|
289
|
+
return self._call_with_fallback("get_many", *args, **kwargs)
|
|
290
|
+
|
|
291
|
+
def get_or_set(self, *args, **kwargs):
|
|
292
|
+
"""
|
|
293
|
+
Pass get_or_set cache method to _call_with_fallback
|
|
294
|
+
"""
|
|
295
|
+
return self._call_with_fallback("get_or_set", *args, **kwargs)
|
|
296
|
+
|
|
297
|
+
def has_key(self, *args, **kwargs):
|
|
298
|
+
"""
|
|
299
|
+
Pass has_key cache method to _call_with_fallback
|
|
300
|
+
"""
|
|
301
|
+
return self._call_with_fallback("has_key", *args, **kwargs)
|
|
302
|
+
|
|
303
|
+
def incr(self, *args, **kwargs):
|
|
304
|
+
"""
|
|
305
|
+
Pass incr cache method to _call_with_fallback
|
|
306
|
+
"""
|
|
307
|
+
return self._call_with_fallback("incr", *args, **kwargs)
|
|
308
|
+
|
|
309
|
+
def decr(self, *args, **kwargs):
|
|
310
|
+
"""
|
|
311
|
+
Pass decr cache method to _call_with_fallback
|
|
312
|
+
"""
|
|
313
|
+
return self._call_with_fallback("decr", *args, **kwargs)
|
|
314
|
+
|
|
315
|
+
def set_many(self, *args, **kwargs):
|
|
316
|
+
"""
|
|
317
|
+
Pass set_many cache method to _call_with_fallback
|
|
318
|
+
"""
|
|
319
|
+
return self._call_with_fallback("set_many", *args, **kwargs)
|
|
320
|
+
|
|
321
|
+
def delete_many(self, *args, **kwargs):
|
|
322
|
+
"""
|
|
323
|
+
Pass delete_many cache method to _call_with_fallback
|
|
324
|
+
"""
|
|
325
|
+
return self._call_with_fallback("delete_many", *args, **kwargs)
|
|
326
|
+
|
|
327
|
+
def clear(self):
|
|
328
|
+
"""
|
|
329
|
+
Pass clear cache method to _call_with_fallback
|
|
330
|
+
"""
|
|
331
|
+
return self._call_with_fallback("clear")
|
|
332
|
+
|
|
333
|
+
def validate_key(self, *args, **kwargs):
|
|
334
|
+
"""
|
|
335
|
+
Pass validate_key cache method to _call_with_fallback
|
|
336
|
+
"""
|
|
337
|
+
return self._call_with_fallback("validate_key", *args, **kwargs)
|
|
338
|
+
|
|
339
|
+
def incr_version(self, *args, **kwargs):
|
|
340
|
+
"""
|
|
341
|
+
Pass incr_version cache method to _call_with_fallback
|
|
342
|
+
"""
|
|
343
|
+
return self._call_with_fallback("incr_version", *args, **kwargs)
|
|
344
|
+
|
|
345
|
+
def decr_version(self, *args, **kwargs):
|
|
346
|
+
"""
|
|
347
|
+
Pass decr_version cache method to _call_with_fallback
|
|
348
|
+
"""
|
|
349
|
+
return self._call_with_fallback("decr_version", *args, **kwargs)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Customizing Django storage backends to enable blue/green deployments."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
|
|
8
|
+
|
|
9
|
+
from storages.backends.s3boto3 import S3Boto3Storage
|
|
10
|
+
|
|
11
|
+
STATIC_POSTPROCESS_IGNORE_REGEX = re.compile(
|
|
12
|
+
r"^richie\/js\/build\/[0-9]*\..*\.index\.js(\.map)?$"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CDNManifestStaticFilesStorage(ManifestStaticFilesStorage):
|
|
17
|
+
"""
|
|
18
|
+
Manifest static files storage backend that can be placed behing a CDN
|
|
19
|
+
and ignores files that are already versioned by webpack.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def post_process(self, paths, dry_run=False, **options): # pylint: disable=W0221
|
|
23
|
+
"""
|
|
24
|
+
Remove paths from file to post process.
|
|
25
|
+
Some js static files generated by webpack already have a unique name per build
|
|
26
|
+
and may be referenced from within the js applications. We therefore don't want
|
|
27
|
+
to hash their name and include them in the manifest file.
|
|
28
|
+
We use a regex configurable via settings to decide which files to ignore.
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
paths : OrderedDict
|
|
32
|
+
List of files to post process
|
|
33
|
+
dry_run: boolean
|
|
34
|
+
run process but nothing is apply if True
|
|
35
|
+
options: kwargs
|
|
36
|
+
See HashedFilesMixin.post_process
|
|
37
|
+
"""
|
|
38
|
+
filtered_paths = OrderedDict()
|
|
39
|
+
for path in paths:
|
|
40
|
+
if not STATIC_POSTPROCESS_IGNORE_REGEX.match(path):
|
|
41
|
+
filtered_paths[path] = paths[path]
|
|
42
|
+
|
|
43
|
+
yield from super().post_process(filtered_paths, dry_run=dry_run, **options)
|
|
44
|
+
|
|
45
|
+
def url(self, name, force=False):
|
|
46
|
+
"""
|
|
47
|
+
Prepend static files path by the CDN base url when configured in settings.
|
|
48
|
+
"""
|
|
49
|
+
url = super().url(name, force=force)
|
|
50
|
+
|
|
51
|
+
cdn_domain = getattr(settings, "CDN_DOMAIN", None)
|
|
52
|
+
if cdn_domain:
|
|
53
|
+
url = f"//{cdn_domain:s}{url:s}"
|
|
54
|
+
|
|
55
|
+
return url
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MediaStorage(S3Boto3Storage): # pylint: disable=W0223
|
|
59
|
+
"""A S3Boto3Storage backend to serve media files via CloudFront."""
|
|
60
|
+
|
|
61
|
+
bucket_name = getattr(settings, "AWS_MEDIA_BUCKET_NAME", None)
|
|
62
|
+
custom_domain = getattr(settings, "CDN_DOMAIN", None)
|
|
63
|
+
file_overwrite = False
|
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
{% load menu_tags %}{% spaceless %}
|
|
1
|
+
{% load cms_tags menu_tags %}{% spaceless %}
|
|
2
2
|
|
|
3
3
|
{% for child in children %}
|
|
4
4
|
{% with children_slug=child.get_menu_title|slugify menu_options=child.menu_extension %}
|
|
5
|
-
<li class="topbar__item
|
|
6
|
-
{% if child.
|
|
5
|
+
<li class="topbar__item
|
|
6
|
+
{% if menu_options.allow_submenu and child.children %}dropdown{% endif %}
|
|
7
|
+
{% if menu_options.allow_submenu and menu_options.menu_color %} topbar__item--{{ menu_options.menu_color }}{% endif %}"
|
|
8
|
+
{% if child.selected or child.attr.redirect_url == current_page.get_absolute_url %} topbar__item--selected{% endif %}
|
|
7
9
|
{% if child.ancestor %} topbar__item--ancestor{% endif %}
|
|
8
10
|
{% if child.sibling %} topbar__item--sibling{% endif %}
|
|
9
|
-
{% if child.descendant %} topbar__item--descendant{% endif %}
|
|
10
|
-
|
|
11
|
-
<a href="{{ child.attr.redirect_url|default:child.get_absolute_url }}">
|
|
12
|
-
{{ child.get_menu_title }}
|
|
13
|
-
</a>
|
|
11
|
+
{% if child.descendant %} topbar__item--descendant{% endif %}"
|
|
12
|
+
>
|
|
14
13
|
{% comment %}Dropdown menu for children are only for page with index page
|
|
15
14
|
extension with a specific option enabled{% endcomment %}
|
|
16
15
|
{% if menu_options.allow_submenu and child.children %}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
<button
|
|
17
|
+
aria-expanded="false"
|
|
18
|
+
aria-haspopup="true"
|
|
19
|
+
aria-controls="dropdown-{{ child.id }}"
|
|
20
|
+
id="dropdown-button-{{ child.id }}"
|
|
21
|
+
>
|
|
22
|
+
{{ child.get_menu_title }}
|
|
23
|
+
<svg class="icon" aria-hidden="true">
|
|
24
|
+
<use xlink:href="#icon-chevron-down"></use>
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
27
|
+
<nav
|
|
28
|
+
class="topbar__sublist"
|
|
29
|
+
id="dropdown-{{ child.id }}"
|
|
30
|
+
aria-labelledby="dropdown-button-{{ child.id }}"
|
|
31
|
+
>
|
|
32
|
+
<ul role="menu">
|
|
33
|
+
{% show_menu from_level to_level extra_inactive extra_active template "" "" child %}
|
|
34
|
+
</ul>
|
|
35
|
+
</nav>
|
|
36
|
+
{% else %}
|
|
37
|
+
<a href="{{ child.attr.redirect_url|default:child.get_absolute_url }}">
|
|
38
|
+
{{ child.get_menu_title }}
|
|
39
|
+
</a>
|
|
20
40
|
{% endif %}
|
|
21
41
|
</li>
|
|
22
42
|
{% endwith %}
|
|
@@ -255,16 +255,158 @@
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Initialize listeners for navigation dropdown menu
|
|
260
|
+
* Add listeners for accessibility features and full-width styling
|
|
261
|
+
* see header_menu.html for the HTML structure
|
|
262
|
+
*/
|
|
263
|
+
function initializeDropdownMenu() {
|
|
264
|
+
const $dropdowns = document.querySelectorAll('.topbar__item.dropdown .topbar__sublist');
|
|
265
|
+
const $dropdownButtons = document.querySelectorAll('.topbar__item.dropdown button');
|
|
266
|
+
const $topbar = document.querySelector('.topbar > .topbar__container');
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Apply full-width class to dropdown menu if it's wider than the topbar
|
|
270
|
+
* To compute the width, we need to take into account the position of the menu button
|
|
271
|
+
* and the width of the topbar
|
|
272
|
+
*/
|
|
273
|
+
function _updateDropdownWidth($dropdown) {
|
|
274
|
+
const $menu = $dropdown.querySelector('ul[role="menu"]');
|
|
275
|
+
const $menuButton = $dropdown.previousElementSibling;
|
|
276
|
+
const topbarWidth = $topbar.getBoundingClientRect().width;
|
|
277
|
+
const topbarLeft = $topbar.getBoundingClientRect().left;
|
|
278
|
+
const menuButtonLeft = $menuButton.getBoundingClientRect().left;
|
|
279
|
+
$dropdown.style.display = 'block';
|
|
280
|
+
$menu.style.flexWrap = 'nowrap';
|
|
281
|
+
const menuWidth = $menu.getBoundingClientRect().width;
|
|
282
|
+
$dropdown.style = '';
|
|
283
|
+
$menu.style = '';
|
|
284
|
+
const menuRelativeOffsetX = menuButtonLeft - topbarLeft;
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if (menuWidth > (topbarWidth - menuRelativeOffsetX)) {
|
|
288
|
+
$dropdown.classList.add('topbar__sublist--full-width');
|
|
289
|
+
} else {
|
|
290
|
+
$dropdown.classList.remove('topbar__sublist--full-width');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function _closeDropdown($button) {
|
|
295
|
+
$button.setAttribute('aria-expanded', 'false');
|
|
296
|
+
// Remove focus trap and event listeners when closing
|
|
297
|
+
const $dropdown = document.getElementById($button.getAttribute('aria-controls'));
|
|
298
|
+
const $menuItems = $dropdown.querySelectorAll('a');
|
|
299
|
+
$menuItems.forEach(item => {
|
|
300
|
+
item.removeEventListener('keydown', _handleMenuItemKeydown);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function _openDropdown($button) {
|
|
305
|
+
$button.setAttribute('aria-expanded', 'true');
|
|
306
|
+
// Set up focus trap and event listeners when opening
|
|
307
|
+
const $dropdown = document.getElementById($button.getAttribute('aria-controls'));
|
|
308
|
+
const $menuItems = $dropdown.querySelectorAll('a');
|
|
309
|
+
$menuItems.forEach(item => {
|
|
310
|
+
item.setAttribute('role', 'menuitem');
|
|
311
|
+
item.addEventListener('keydown', _handleMenuItemKeydown);
|
|
312
|
+
});
|
|
313
|
+
// Focus first menu item
|
|
314
|
+
if ($menuItems.length) $menuItems[0].focus();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Allow navigation through dropdown menu items using keyboard
|
|
319
|
+
*/
|
|
320
|
+
function _handleMenuItemKeydown(event) {
|
|
321
|
+
const $currentItem = event.target;
|
|
322
|
+
const $dropdown = $currentItem.closest('.topbar__sublist');
|
|
323
|
+
const $menuItems = Array.from($dropdown.querySelectorAll('a'));
|
|
324
|
+
const currentIndex = $menuItems.indexOf($currentItem);
|
|
325
|
+
const $button = document.querySelector(`[aria-controls="${$dropdown.id}"]`);
|
|
326
|
+
|
|
327
|
+
switch (event.key) {
|
|
328
|
+
case 'ArrowDown':
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
const nextIndex = (currentIndex + 1) % $menuItems.length;
|
|
331
|
+
$menuItems[nextIndex].focus();
|
|
332
|
+
break;
|
|
333
|
+
case 'ArrowUp':
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
const prevIndex = (currentIndex - 1 + $menuItems.length) % $menuItems.length;
|
|
336
|
+
$menuItems[prevIndex].focus();
|
|
337
|
+
break;
|
|
338
|
+
case 'Escape':
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
_closeDropdown($button);
|
|
341
|
+
$button.focus();
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
$dropdowns.forEach(($dropdown, index) => {
|
|
347
|
+
const $button = $dropdownButtons[index];
|
|
348
|
+
|
|
349
|
+
// Initial width calculation
|
|
350
|
+
_updateDropdownWidth($dropdown);
|
|
351
|
+
|
|
352
|
+
// Handle button clicks
|
|
353
|
+
$button.addEventListener('click', () => {
|
|
354
|
+
const isExpanded = $button.getAttribute('aria-expanded') === 'true';
|
|
355
|
+
if (isExpanded) {
|
|
356
|
+
_closeDropdown($button);
|
|
357
|
+
} else {
|
|
358
|
+
// Close any other open dropdowns
|
|
359
|
+
$dropdownButtons.forEach(btn => {
|
|
360
|
+
if (btn !== $button && btn.getAttribute('aria-expanded') === 'true') {
|
|
361
|
+
_closeDropdown(btn);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
_openDropdown($button);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Handle button keyboard interactions
|
|
369
|
+
$button.addEventListener('keydown', (event) => {
|
|
370
|
+
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
|
|
371
|
+
event.preventDefault();
|
|
372
|
+
_openDropdown($button);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Close when clicking outside
|
|
377
|
+
document.addEventListener('click', (event) => {
|
|
378
|
+
if (!$button.contains(event.target) && !$dropdown.contains(event.target)) {
|
|
379
|
+
_closeDropdown($button);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Close when element within dropdown focus is lost
|
|
384
|
+
$dropdown.addEventListener('focusout', (event) => {
|
|
385
|
+
const $focusedElement = event.relatedTarget;
|
|
386
|
+
if (!$dropdown.contains($focusedElement)) {
|
|
387
|
+
_closeDropdown($button);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Handle window resize
|
|
393
|
+
let resizeTimeout;
|
|
394
|
+
const RESIZE_DEBOUNCE_DELAY = 250;
|
|
395
|
+
window.addEventListener('resize', () => {
|
|
396
|
+
clearTimeout(resizeTimeout);
|
|
397
|
+
resizeTimeout = setTimeout(() => {
|
|
398
|
+
$dropdowns.forEach($dropdown => {
|
|
399
|
+
_updateDropdownWidth($dropdown);
|
|
400
|
+
});
|
|
401
|
+
}, RESIZE_DEBOUNCE_DELAY);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
258
405
|
document.addEventListener("DOMContentLoaded", function() {
|
|
259
406
|
initializeAccordions();
|
|
260
407
|
initializeHamburgerMenu();
|
|
408
|
+
initializeDropdownMenu();
|
|
261
409
|
});
|
|
262
|
-
|
|
263
|
-
function toggleClass(elements, clazz) {
|
|
264
|
-
for (let element of elements) {
|
|
265
|
-
element.classList.toggle(clazz);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
410
|
</script>
|
|
269
411
|
{% if request.toolbar and request.toolbar.edit_mode_active %}
|
|
270
412
|
{# When edit mode is active, we have to refresh js scripts after saving modifications #}
|
|
@@ -274,6 +416,7 @@
|
|
|
274
416
|
__RICHIE__.render();
|
|
275
417
|
initializeAccordions();
|
|
276
418
|
initializeHamburgerMenu();
|
|
419
|
+
initializeDropdownMenu();
|
|
277
420
|
});
|
|
278
421
|
</script>
|
|
279
422
|
{% endif %}
|