django-website-widgets 0.1.0__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.
@@ -0,0 +1,514 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-website-widgets
3
+ Version: 0.1.0
4
+ Summary: Server-rendered pages built from composable widgets, for Django
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: django,widgets,components,pages
8
+ Author: Nate Brooks
9
+ Requires-Python: >=3.12
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Framework :: Django :: 6.0
15
+ Classifier: Framework :: Django :: 6.1
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
25
+ Requires-Dist: django (>=5.2)
26
+ Project-URL: Homepage, https://github.com/nateonguitar/django-website-widgets
27
+ Project-URL: Issues, https://github.com/nateonguitar/django-website-widgets/issues
28
+ Project-URL: Repository, https://github.com/nateonguitar/django-website-widgets
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Django Website Widgets
32
+
33
+ A reusable Django app for building component-based web interfaces with widgets and pages.
34
+
35
+ ## Installation
36
+
37
+ Requires Python 3.12+ and Django 5.2+.
38
+
39
+ ```bash
40
+ pip install django-website-widgets
41
+ ```
42
+
43
+ Or with Poetry:
44
+
45
+ ```bash
46
+ poetry add django-website-widgets
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ 1. Add to `INSTALLED_APPS` in `settings.py`:
52
+
53
+ ```python
54
+ INSTALLED_APPS = [
55
+ ...
56
+ 'website_widgets',
57
+ ]
58
+ ```
59
+
60
+ 2. Create a document template for all pages to use: the whole HTML document, `<head>` included.
61
+ Customize it as needed with stylesheets, an appbar, a footer, etc.
62
+
63
+ `website/document_template.html`
64
+ ```html
65
+ {% load static %}
66
+ <!DOCTYPE html>
67
+ <html lang="en">
68
+ <head>
69
+ <meta charset="UTF-8">
70
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
71
+ <title>{{ page.title|default:"Foo" }}</title>
72
+ <link rel="stylesheet" href="{% static 'styles.css' %}">
73
+ </head>
74
+ <body>
75
+ {{ children.appbar }}
76
+
77
+ <main id="main-content">
78
+ {{ page_content_html }}
79
+ </main>
80
+ </body>
81
+ </html>
82
+
83
+ ```
84
+
85
+ 3. Create a `Page` subclass for every other page to extend, pointing at that template:
86
+
87
+ `website/app_page.py`
88
+ ```python
89
+ import os
90
+
91
+ from website_widgets import Page
92
+ from website.widgets.appbar import Appbar
93
+
94
+ class AppPage(Page):
95
+ # Absolute: a relative path resolves beside each concrete page, not beside this file.
96
+ document_template_path = os.path.join(os.path.dirname(__file__), 'document_template.html')
97
+
98
+ async def set_context(self):
99
+ # Construct widget - system will render it automatically
100
+ self.context.children['appbar'] = Appbar()
101
+ ```
102
+
103
+ A page renders two templates against the same context: its own, then the document template, which
104
+ receives the first as `{{ page_content_html }}`. So `children.appbar` can render in either one.
105
+ Here it sits outside the page's content but still reads `context.page`, which every page fills
106
+ before either template renders:
107
+
108
+ `website/widgets/appbar/appbar.html`
109
+ ```html
110
+ <header class="appbar">
111
+ <a href="/">Home</a>
112
+ <span>{{ page.title }}</span>
113
+ </header>
114
+ ```
115
+
116
+ Where the appbar goes is the project's choice: every page can set its own
117
+ `children['appbar']`, a page can leave it out, or the document template can hold the markup
118
+ directly.
119
+
120
+ 4. Create a widget and a template next to it, named after the widget's Python file:
121
+
122
+ `website/widgets/project_details_card/project_details_card.py`
123
+ ```python
124
+ from website_widgets import Widget
125
+ from project.models import Project
126
+
127
+ class ProjectDetailsCard(Widget):
128
+ """Widget that displays project details."""
129
+
130
+ def __init__(self, project: Project):
131
+ super().__init__()
132
+ self.context.data['project'] = project
133
+
134
+ def set_context(self):
135
+ """Called before rendering. Can be sync or async."""
136
+ super().set_context()
137
+ ```
138
+
139
+ `website/widgets/project_details_card/project_details_card.html`
140
+ ```html
141
+ <div class="project-details-card">
142
+ <p>Name: {{ data.project.name }}</p>
143
+ </div>
144
+ ```
145
+
146
+ 5. Create a page and its template the same way:
147
+
148
+ `website/pages/project_page/project_page.py`
149
+ ```python
150
+ from project.models import Project
151
+ from website.app_page import AppPage
152
+ from website.widgets.project_details_card import ProjectDetailsCard
153
+
154
+ class ProjectPage(AppPage):
155
+ """Page that displays a single project."""
156
+
157
+ def __init__(self, pk: int):
158
+ super().__init__()
159
+ self.pk = pk
160
+
161
+ async def set_context(self):
162
+ await super().set_context()
163
+
164
+ project = await Project.objects.aget(pk=self.pk)
165
+ self.context.page['title'] = f'Project - {project.name}'
166
+ self.context.data['project'] = project
167
+
168
+ # Construct widgets - system will render them automatically
169
+ self.context.children['project_details_card'] = ProjectDetailsCard(
170
+ project=project
171
+ )
172
+ ```
173
+
174
+
175
+ `website/pages/project_page/project_page.html`
176
+ ```html
177
+ <div id="project-details-page">
178
+ {{ children.project_details_card }}
179
+
180
+ <p>Other content here</p>
181
+ </div>
182
+ ```
183
+
184
+ 6. Register in `urls.py`:
185
+
186
+ ```python
187
+ from django.urls import path
188
+ from .pages.project_page import ProjectPage
189
+
190
+ urlpatterns = [
191
+ path('project/<int:pk>', ProjectPage.as_view(), name='project_page'),
192
+ ]
193
+ ```
194
+
195
+ ## Core Concepts
196
+
197
+ ### Widget
198
+
199
+ Base class for reusable UI components. Uses a declarative, constructor-based pattern similar to Flutter or React.
200
+
201
+ **Key Pattern:**
202
+ - Accept data via `__init__()` parameters
203
+ - Store data on instance variables
204
+ - Implement `set_context()` to populate `self.context.data` and create child widgets
205
+ - System automatically renders widgets after initialization
206
+
207
+ **Example:**
208
+ ```python
209
+ class InfoCard(Widget):
210
+ def __init__(self, title: str, content: str):
211
+ super().__init__()
212
+ self.title = title
213
+ self.content = content
214
+
215
+ def set_context(self):
216
+ self.context.data['title'] = self.title
217
+ self.context.data['content'] = self.content
218
+ ```
219
+
220
+ Widgets:
221
+ - Accept data parameters in `__init__()` and store them
222
+ - Use `set_context()` to create child widgets (Widget objects or lists)
223
+ - Store children in `self.context.children` - system renders them
224
+ - Access `self.request` and `self.request.user` from `set_context()` onwards (not in `__init__()`)
225
+ - Can be sync or async - use `async def set_context(self):` only when awaiting. Rendering always
226
+ runs in an async view, so database access in a sync `set_context()` raises
227
+ `SynchronousOnlyOperation`; use the async ORM
228
+
229
+ ### Page
230
+
231
+ Extends `Widget` and represents a full page.
232
+
233
+ **Key Pattern:**
234
+ - URL parameters passed to `__init__()` - store them on self
235
+ - `set_context()` loads data and creates child widgets
236
+ - Assign Widget objects (or lists) to `self.context.children`
237
+ - System calls `set_context()` recursively, then renders all widgets
238
+
239
+ **Example with URL parameters:**
240
+ ```python
241
+ class ProfilePage(AppPage):
242
+ def __init__(self, username: str):
243
+ super().__init__()
244
+ self.username = username
245
+
246
+ async def set_context(self):
247
+ await super().set_context()
248
+ user = await User.objects.aget(username=self.username)
249
+ self.context.page['title'] = f'Profile: {user.username}'
250
+
251
+ # Construct widgets; the page renders them
252
+ self.context.children['profile_card'] = ProfileCard(user=user)
253
+ ```
254
+
255
+ Pages:
256
+ - Set `document_template_path` for the HTML document wrapping their own template
257
+ - Receive URL parameters in `__init__()`
258
+ - Use `set_context()` to load data and construct child widgets
259
+ - Route with `@register_url` (see below), or by hand in `urls.py` with `.as_view()`
260
+ - Override `handle_request()` for authentication, redirects or conditional responses, and
261
+ `build_response()` for headers
262
+
263
+ ### WidgetContext
264
+
265
+ Each widget has `self.context`, a `WidgetContext`, with:
266
+ - `data`: Template variables (dict)
267
+ - `children`: Child widgets - values can be Widget, list[Widget], or None
268
+ - `page`: Shared page-level data (title, meta, etc.)
269
+
270
+ System renders widgets to HTML automatically. Lists iterate in templates via `{% for item in children.items %}`.
271
+
272
+ ### Rendering a widget on its own
273
+
274
+ Outside a page, for an endpoint returning HTML fragments (an infinite scroller, a "load more"
275
+ button), construct the widget, adjust it, and render it once:
276
+
277
+ ```python
278
+ widget = ProjectListTile(project=project)
279
+ widget.context.page['nonce'] = nonce
280
+ html = await widget.render__standalone(request)
281
+ ```
282
+
283
+ Tests render a widget directly against a mock request, optionally as a given user:
284
+
285
+ ```python
286
+ html = await ProjectListTile(project=project).render__test(user=user) # async def test
287
+ html = ProjectListTile(project=project).render__test_sync(user=user) # def test
288
+ ```
289
+
290
+ Each runs the widget's `set_context()` and every descendant's, then the templates. No document
291
+ template wraps the result.
292
+
293
+ ### Routing pages with `@register_url`
294
+
295
+ A page can declare its own routes instead of being listed in `urls.py`. The decorator takes
296
+ `path()`'s arguments, and stacks one per route:
297
+
298
+ ```python
299
+ from website_widgets import register_url
300
+
301
+ @register_url('')
302
+ class HomePage(AppPage): ...
303
+
304
+ @register_url('forum/<int:post_id>', name='forum_post')
305
+ @register_url('tab-requests/<int:post_id>', name='tab_request')
306
+ class ForumPostPage(AppPage): ...
307
+
308
+ @register_url('forum/create-post', name='forum_create_post', kwargs={'post_type': 'forum_post'})
309
+ class ForumCreatePostPage(AppPage): ...
310
+ ```
311
+
312
+ Then include every decorated page with one line, the way the admin is included:
313
+
314
+ ```python
315
+ from website_widgets import PageRoutes
316
+
317
+ urlpatterns = [
318
+ path('', PageRoutes.urls()),
319
+ ...
320
+ ]
321
+ ```
322
+
323
+ - **Pages are found by importing every module under the website app's `pages/` folder**, the app
324
+ being `WebsiteWidgetsConfig.website_app_name` (default `'website'`). A page elsewhere isn't found.
325
+ - **Routes live in a namespace named after that app.** `name` defaults to the class name, so
326
+ `HomePage` reverses as `reverse('website:HomePage')`, and `forum_post` as `'website:forum_post'`.
327
+ - **`kwargs` reach the page's constructor** alongside the URL's parameters.
328
+ - **Routes are tried most specific first, never in file order.** They are compared segment by
329
+ segment: a fixed segment comes before `int`, then `uuid`, `slug`, `str` and `path`, and equally
330
+ specific routes fall back to alphabetical order. So `profile/settings` is always tried before
331
+ `profile/<str:username>`, wherever either is declared.
332
+ - **Two equally specific routes that can match the same URL fail a system check**
333
+ (`website_widgets.E001`), such as `a/<str:x>` and `a/<str:y>`.
334
+ - **A decorated route is an ordinary `path()`.** `reverse()`, `{% url %}`, `resolve()` and URL
335
+ tools such as django-extensions' `show_urls` all see it. Routing a page by hand with
336
+ `path('...', MyPage.as_view(), name='...')` still works, and the two mix freely; a hand-routed
337
+ page's name sits outside the namespace.
338
+
339
+ To see the order routes are tried in, and check that nothing clobbers them, run
340
+ `show_page_resolve_order`:
341
+
342
+ ```
343
+ $ python manage.py show_page_resolve_order
344
+ # URL Segments Page
345
+ 1 /moderation/reports/<int:report_id> f/f/i ViewReportPage
346
+ ...
347
+ 15 /profile/<str:username> f/s UserProfilePage
348
+ ...
349
+ 17 / f HomePage
350
+
351
+ Segments: f fixed, i int, s str, g slug, u uuid, p path, and a project converter by its name.
352
+ All 37 page routes reach their page.
353
+ ```
354
+
355
+ For each route it reverses a sample URL (`/profile/a`) and resolves it through the whole project,
356
+ so it also catches a route that never reaches its page because something earlier in `urls.py`
357
+ takes its URLs, such as a hand-written route or a catch-all, or because `PageRoutes.urls()` isn't
358
+ included. A route that fails prints in red with the reason, and the command exits non-zero, so it
359
+ can run in CI.
360
+
361
+ - `--columns` picks the columns after `#`, in order, from `url,segments,name,page,reaches_page`
362
+ (default `url,segments,page`). The reachability check runs either way.
363
+ - `--path-param-type` shows a path parameter as declared (`full`, the default:
364
+ `<str:game_slug>`), as its converter (`type`: `<str>`) or as the sample value the check uses
365
+ (`example`: `a`).
366
+
367
+ ### URL templates for the frontend
368
+
369
+ `reverse_template()` stands in for `reverse()` when the frontend fills in the parameters. It
370
+ reverses a URL name into a template with a `{name}` placeholder per route parameter:
371
+
372
+ ```python
373
+ from website_widgets import reverse_template
374
+
375
+ # path('projects/<int:project_id>/tasks/<slug:task_slug>', ..., name='project_task')
376
+ reverse_template('project_task') # '/projects/{project_id}/tasks/{task_slug}'
377
+ reverse_template('project_list') # '/projects', as reverse() returns it
378
+ ```
379
+
380
+ Placeholders take the route's own parameter names, and namespaces, `include()` prefixes and the
381
+ script prefix apply as they do in `reverse()`. Converters don't run, so a placeholder is accepted
382
+ wherever a parameter goes. Pass `paramnames` to rename the placeholders in route order, which a
383
+ `re_path()` with unnamed groups needs: `reverse_template('legacy', ['legacy_id'])`. A name whose
384
+ routes take different parameters raises `ValueError` unless `paramnames` picks one by count.
385
+
386
+ ## Templates
387
+
388
+ A widget's template sits next to its Python file and shares its name: `my_widget.py` renders
389
+ `my_widget.html`. Set `template_filename` to use a different file, either on the class or in
390
+ `set_context()` to pick between files at runtime.
391
+
392
+ **`template_filename` is a bare file name, never a path.** A template lives beside the code that
393
+ chooses it, so a `/`, a `\` or `..` raises `ValueError`. The file is looked up beside the class's
394
+ own `.py` file first, then beside each base class's file in inheritance order, and the first match
395
+ wins. That lets a base class keep a variant beside its own module and switch any subclass to it.
396
+ A name found nowhere raises `FileNotFoundError` listing every folder searched.
397
+
398
+ ```python
399
+ class ProjectPage(AppPage):
400
+ async def set_context(self):
401
+ await super().set_context()
402
+ project = await Project.objects.filter(pk=self.pk).afirst()
403
+ if project is None:
404
+ self.status_code = 404
405
+ self.template_filename = 'project_page__not_found.html'
406
+ return
407
+ ...
408
+ ```
409
+
410
+ **Suggested convention: name a variant `<module>__<variant>.html`**, with a double underscore,
411
+ beside the main template:
412
+
413
+ ```
414
+ project_page/
415
+ project_page.py
416
+ project_page.html
417
+ project_page__not_found.html
418
+ project_page__archived.html
419
+ ```
420
+
421
+ The double underscore marks where the module name ends, the way it separates the parts of a Django
422
+ lookup (`author__name__startswith`): module and variant names both contain single underscores, so
423
+ one alone can't. The prefix keeps every variant sorted next to the template it replaces, and an
424
+ editor tab or a search result names its component without the folder. Give the variant the same
425
+ root element as the main template, so the component's styles still apply to it.
426
+
427
+ A page's document template is the one template set by path: `document_template_path` names a file
428
+ shared by every page, so it lives beside the shared `Page` subclass rather than beside any one page.
429
+
430
+ Override `get_template_path()` when the choice needs more than an attribute.
431
+
432
+ ```html
433
+ <div class="my-widget">
434
+ <h1>{{ data.title }}</h1>
435
+ {{ children.my_widget }}
436
+ </div>
437
+ ```
438
+
439
+ Templates are opened directly rather than through Django's template loader, so context processors
440
+ do not run. The context holds only `request`, `user`, `page`, `data` and `children`. See
441
+ `docs/rendering_pipeline.md` for the full render order.
442
+
443
+ ## Configuration
444
+
445
+ Configure django-website-widgets in your `settings.py`:
446
+
447
+ ```python
448
+ from website_widgets.config import WebsiteWidgetsConfig
449
+
450
+ WEBSITE_WIDGETS = WebsiteWidgetsConfig(
451
+ show_debug_tree=True, # Show widget tree visualization on pages (default: False)
452
+ website_app_name='website', # Label of the app holding your pages/ (default: 'website')
453
+ )
454
+ ```
455
+
456
+ If `WEBSITE_WIDGETS` is not defined, a default configuration is used automatically. The setting is
457
+ read once per process, so changing it at runtime has no effect.
458
+
459
+ ### Debug Tree Visualization
460
+
461
+ Enable `show_debug_tree=True` to display a visual representation of your widget hierarchy after each page's main content. This is useful during development to understand the structure of your widget tree.
462
+
463
+ **Example output:**
464
+ ```
465
+ CompositionPage
466
+ stylesheet:
467
+ AdvancedStylesheet
468
+ constructor_card:
469
+ InfoCard
470
+ list_cards: [
471
+ InfoCard
472
+ InfoCard
473
+ InfoCard
474
+ ]
475
+ skipped_card: None
476
+ page_title_note:
477
+ PageTitleNote
478
+ ```
479
+
480
+ #### Adding Debug Labels
481
+
482
+ For better debugging, add a `_debug_label` to your widgets:
483
+
484
+ ```python
485
+ class InfoCard(Widget):
486
+ def __init__(self, title: str, content: str):
487
+ super().__init__()
488
+ self._debug_label = title # Shows as "InfoCard(My Title)" in debug tree
489
+ self.title = title
490
+ self.content = content
491
+ ```
492
+
493
+ Without `_debug_label`, widgets display only their class name (e.g., `InfoCard`). With the label, they display as `InfoCard(My Title)`, making it easier to identify specific widget instances in complex trees.
494
+
495
+ **Recommendation:** Only enable `show_debug_tree` in development environments, not in production.
496
+
497
+ ## Management Commands
498
+
499
+ Check installation:
500
+ ```bash
501
+ python manage.py test_website_widgets --verbose
502
+ ```
503
+
504
+ List the `@register_url` routes in the order they are tried, and check each reaches its page
505
+ (see "Routing pages with `@register_url`"):
506
+ ```bash
507
+ python manage.py show_page_resolve_order
508
+ python manage.py show_page_resolve_order --columns=url,name,page,reaches_page --path-param-type=type
509
+ ```
510
+
511
+ ## License
512
+
513
+ MIT
514
+
@@ -0,0 +1,17 @@
1
+ website_widgets/__init__.py,sha256=FyDUWgMvJPWofFLmlNOj0kR1cvp6rr6u2a0vRJDmt30,336
2
+ website_widgets/apps.py,sha256=azmjoN6qrZWtfjxpTnjW4e1sMf3vCHCEBGZdRiBUl8Q,352
3
+ website_widgets/config.py,sha256=shtZBEr51LpFJ96yHPybSMMBMJHUkCFUs2tyUak9SSI,1020
4
+ website_widgets/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ website_widgets/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ website_widgets/management/commands/show_page_resolve_order.py,sha256=WObtZ6b40sj_uc4lMdJwJih_xG_TK6ZhFVgpWQDelMw,7375
7
+ website_widgets/management/commands/test_website_widgets.py,sha256=82j7MOlC-xzQ8hLw8RF8dsN5MtPgliLsf9pwbQ5vXlo,2084
8
+ website_widgets/page.py,sha256=1w4NLSB34BklAXNUqqIfcajvCAl2BsZ7219z5xXz1sk,5023
9
+ website_widgets/page_routes.py,sha256=BxTHHjhuaRlKRVWE2900ZhSFoYqeAdlqNCcShY9l08w,10283
10
+ website_widgets/tests.py,sha256=AgdkSgbVu0HqoxBnO03g7ahQudCitb8OIZfYxaOFhwU,25854
11
+ website_widgets/url_templates.py,sha256=ftCiKvMBOKoJxk_gbtzAeOw6gbbDo9INrcCLOT6HcwI,3058
12
+ website_widgets/widget.py,sha256=URz8IHx-suWMt0Shc3d47UVqKhezbMxqJ1UpijB-VzE,12346
13
+ website_widgets/widget_context.py,sha256=-3O05yaM3u4YWFk9XarRUEwRQD5Ijf00J_NOleIl3D0,682
14
+ django_website_widgets-0.1.0.dist-info/METADATA,sha256=a_1JFSeMmSAqFXY8uak2gS6I77q4Wtwuu-sMgwjH3TM,17883
15
+ django_website_widgets-0.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
16
+ django_website_widgets-0.1.0.dist-info/licenses/LICENSE,sha256=qB736cZgYSvxLrQG-Vv7zP-YixV9NdobEx26j--srFo,1068
17
+ django_website_widgets-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nate Brooks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ from .widget import Widget
2
+ from .page import Page
3
+ from .widget_context import WidgetContext
4
+ from .url_templates import reverse_template
5
+ from .page_routes import PageRoutes
6
+
7
+ register_url = PageRoutes.register_url
8
+
9
+ __all__ = [
10
+ 'Widget',
11
+ 'Page',
12
+ 'WidgetContext',
13
+ 'reverse_template',
14
+ 'PageRoutes',
15
+ 'register_url',
16
+ ]
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class WebsiteWidgetsConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'website_widgets'
7
+
8
+ def ready(self):
9
+ from django.core import checks
10
+
11
+ from website_widgets.page_routes import PageRoutes
12
+
13
+ checks.register(PageRoutes.check_for_ties, checks.Tags.urls)
@@ -0,0 +1,36 @@
1
+ class WebsiteWidgetsConfig:
2
+ def __init__(
3
+ self,
4
+ show_debug_tree: bool = False,
5
+ website_app_name: str = 'website',
6
+ ):
7
+ self.show_debug_tree = show_debug_tree
8
+ # An app label, resolved through the app registry, so nothing depends on the working
9
+ # directory.
10
+ self.website_app_name = website_app_name
11
+
12
+
13
+ _config_cache: WebsiteWidgetsConfig | None = None
14
+
15
+
16
+ def get_config() -> WebsiteWidgetsConfig:
17
+ """
18
+ Get the website widgets configuration from Django settings.
19
+
20
+ Caches the configuration on first access. If settings.WEBSITE_WIDGETS is not defined,
21
+ uses a default configuration.
22
+
23
+ Returns:
24
+ WebsiteWidgetsConfig: The configuration object
25
+ """
26
+ global _config_cache
27
+
28
+ if _config_cache is None:
29
+ from django.conf import settings
30
+
31
+ if hasattr(settings, 'WEBSITE_WIDGETS'):
32
+ _config_cache = settings.WEBSITE_WIDGETS
33
+ else:
34
+ _config_cache = WebsiteWidgetsConfig()
35
+
36
+ return _config_cache
File without changes
File without changes