django-simple-page 1.0.0__tar.gz
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.
- django_simple_page-1.0.0/LICENCE +27 -0
- django_simple_page-1.0.0/PKG-INFO +98 -0
- django_simple_page-1.0.0/README.md +59 -0
- django_simple_page-1.0.0/django_simple_page.egg-info/PKG-INFO +98 -0
- django_simple_page-1.0.0/django_simple_page.egg-info/SOURCES.txt +19 -0
- django_simple_page-1.0.0/django_simple_page.egg-info/dependency_links.txt +1 -0
- django_simple_page-1.0.0/django_simple_page.egg-info/requires.txt +8 -0
- django_simple_page-1.0.0/django_simple_page.egg-info/top_level.txt +1 -0
- django_simple_page-1.0.0/pyproject.toml +60 -0
- django_simple_page-1.0.0/setup.cfg +4 -0
- django_simple_page-1.0.0/simple_page/__init__.py +75 -0
- django_simple_page-1.0.0/simple_page/__version__.py +16 -0
- django_simple_page-1.0.0/simple_page/admin.py +199 -0
- django_simple_page-1.0.0/simple_page/apps.py +11 -0
- django_simple_page-1.0.0/simple_page/forms.py +13 -0
- django_simple_page-1.0.0/simple_page/models.py +192 -0
- django_simple_page-1.0.0/simple_page/renderer.py +294 -0
- django_simple_page-1.0.0/simple_page/signals.py +15 -0
- django_simple_page-1.0.0/simple_page/templates/admin/simple_page/page/change_form.html +19 -0
- django_simple_page-1.0.0/simple_page/templates/simple_page/menu.html +18 -0
- django_simple_page-1.0.0/simple_page/views.py +22 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2020, Thomas Leichtfuß.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
5
|
+
are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
8
|
+
this list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
documentation and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the author nor the names of contributors
|
|
15
|
+
may be used to endorse or promote products derived from this software
|
|
16
|
+
without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
22
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
25
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-simple-page
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Author-email: Thomas Leichtfuß <thomas.leichtfuss@posteo.de>
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Project-URL: Homepage, https://github.com/thomst/django-simple-page
|
|
7
|
+
Project-URL: Repository, https://github.com/thomst/django-simple-page
|
|
8
|
+
Project-URL: Documentation, https://github.com/thomst/django-simple-page#readme
|
|
9
|
+
Keywords: django,django-admin,cms,website
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 3.2
|
|
13
|
+
Classifier: Framework :: Django :: 4.0
|
|
14
|
+
Classifier: Framework :: Django :: 4.1
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.0
|
|
17
|
+
Classifier: Framework :: Django :: 5.1
|
|
18
|
+
Classifier: Framework :: Django :: 5.2
|
|
19
|
+
Classifier: Framework :: Django :: 6.0
|
|
20
|
+
Classifier: Environment :: Web Environment
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python
|
|
24
|
+
Classifier: Programming Language :: Python :: 3
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
+
Classifier: Topic :: Software Development
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENCE
|
|
31
|
+
Requires-Dist: Django>=3.2
|
|
32
|
+
Requires-Dist: django-mptt
|
|
33
|
+
Requires-Dist: django-model-utils
|
|
34
|
+
Requires-Dist: django-reorder_items_widget
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: Pillow; extra == "test"
|
|
37
|
+
Requires-Dist: coverage; extra == "test"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Welcome to django-simple-page
|
|
41
|
+
|
|
42
|
+
[](https://github.com/thomst/django-simple-page/actions/workflows/tests.yml)
|
|
43
|
+
[](https://coveralls.io/github/thomst/django-simple-page?branch=main)
|
|
44
|
+
[<img src="https://img.shields.io/badge/django-3.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-orange">](https://img.shields.io/badge/django-3.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-orange)
|
|
45
|
+
|
|
46
|
+
Django-simple-page is a cms buildkit for your website. The strength of this
|
|
47
|
+
project is its simplicity - using comprehensible yet powerful concepts. You get
|
|
48
|
+
the basic stuff, but retain all your freedom.
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- [github](https://github.com/django-simple-page/)
|
|
53
|
+
- [docs](https://thomst.github.io/django-simple-page/)
|
|
54
|
+
- [pypi](https://pypi.org/project/django-simple-page/)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Tree structured Pages**: By django-mptt.
|
|
60
|
+
- **Pages, regions and sections**: Assigning sections to regions on pages.
|
|
61
|
+
- **Custom rendering logic**: Each page or section can have its own renderer.
|
|
62
|
+
- **Simple yet powerful concept**: Everything can be customized by subclassing.
|
|
63
|
+
- **Admin backend integration**: Easy to use. Order elements via drag and drop.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Description
|
|
67
|
+
|
|
68
|
+
### Pages, regions and sections
|
|
69
|
+
|
|
70
|
+
You got a reliable database layout of pages and sections. Sections are
|
|
71
|
+
associated with regions on pages. Everything else is up to you. Sections could
|
|
72
|
+
be anything you want, from a simple content type like an article with title and
|
|
73
|
+
text body to a full featured gallery. You build what you need just by
|
|
74
|
+
subclassing the page and section model.
|
|
75
|
+
|
|
76
|
+
### Renderer
|
|
77
|
+
|
|
78
|
+
While there are default renderers for pages and sections which are probably
|
|
79
|
+
suitable for most use cases, you are free to completely adapt or overwrite them.
|
|
80
|
+
Each page or section can have its own renderer providing a specific rendering
|
|
81
|
+
logic. And each renderer can have its own Media class defining javascript or css
|
|
82
|
+
files. Those media assets are merged by the page renderer and be available as
|
|
83
|
+
`media` template variable.
|
|
84
|
+
|
|
85
|
+
### Admin integration
|
|
86
|
+
|
|
87
|
+
At least we provide a handy admin backend integration. Rearrange your pages by
|
|
88
|
+
drag and drop. Add sections to your page regions with inline formsets and
|
|
89
|
+
reorder them by just dragging them to their new position. It's simple and
|
|
90
|
+
sufficient.
|
|
91
|
+
|
|
92
|
+
### Summing-up
|
|
93
|
+
|
|
94
|
+
As you can see, everything is done by subclassing. While django-simple-page
|
|
95
|
+
giving you the basics to build your website, it is not taking any freedom from
|
|
96
|
+
you. You define your pages with regions, your sections as content, your
|
|
97
|
+
rendering logic with their media classes and put everything together like
|
|
98
|
+
building blocks.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Welcome to django-simple-page
|
|
2
|
+
|
|
3
|
+
[](https://github.com/thomst/django-simple-page/actions/workflows/tests.yml)
|
|
4
|
+
[](https://coveralls.io/github/thomst/django-simple-page?branch=main)
|
|
5
|
+
[<img src="https://img.shields.io/badge/django-3.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-orange">](https://img.shields.io/badge/django-3.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-orange)
|
|
6
|
+
|
|
7
|
+
Django-simple-page is a cms buildkit for your website. The strength of this
|
|
8
|
+
project is its simplicity - using comprehensible yet powerful concepts. You get
|
|
9
|
+
the basic stuff, but retain all your freedom.
|
|
10
|
+
|
|
11
|
+
## Links
|
|
12
|
+
|
|
13
|
+
- [github](https://github.com/django-simple-page/)
|
|
14
|
+
- [docs](https://thomst.github.io/django-simple-page/)
|
|
15
|
+
- [pypi](https://pypi.org/project/django-simple-page/)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Tree structured Pages**: By django-mptt.
|
|
21
|
+
- **Pages, regions and sections**: Assigning sections to regions on pages.
|
|
22
|
+
- **Custom rendering logic**: Each page or section can have its own renderer.
|
|
23
|
+
- **Simple yet powerful concept**: Everything can be customized by subclassing.
|
|
24
|
+
- **Admin backend integration**: Easy to use. Order elements via drag and drop.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Description
|
|
28
|
+
|
|
29
|
+
### Pages, regions and sections
|
|
30
|
+
|
|
31
|
+
You got a reliable database layout of pages and sections. Sections are
|
|
32
|
+
associated with regions on pages. Everything else is up to you. Sections could
|
|
33
|
+
be anything you want, from a simple content type like an article with title and
|
|
34
|
+
text body to a full featured gallery. You build what you need just by
|
|
35
|
+
subclassing the page and section model.
|
|
36
|
+
|
|
37
|
+
### Renderer
|
|
38
|
+
|
|
39
|
+
While there are default renderers for pages and sections which are probably
|
|
40
|
+
suitable for most use cases, you are free to completely adapt or overwrite them.
|
|
41
|
+
Each page or section can have its own renderer providing a specific rendering
|
|
42
|
+
logic. And each renderer can have its own Media class defining javascript or css
|
|
43
|
+
files. Those media assets are merged by the page renderer and be available as
|
|
44
|
+
`media` template variable.
|
|
45
|
+
|
|
46
|
+
### Admin integration
|
|
47
|
+
|
|
48
|
+
At least we provide a handy admin backend integration. Rearrange your pages by
|
|
49
|
+
drag and drop. Add sections to your page regions with inline formsets and
|
|
50
|
+
reorder them by just dragging them to their new position. It's simple and
|
|
51
|
+
sufficient.
|
|
52
|
+
|
|
53
|
+
### Summing-up
|
|
54
|
+
|
|
55
|
+
As you can see, everything is done by subclassing. While django-simple-page
|
|
56
|
+
giving you the basics to build your website, it is not taking any freedom from
|
|
57
|
+
you. You define your pages with regions, your sections as content, your
|
|
58
|
+
rendering logic with their media classes and put everything together like
|
|
59
|
+
building blocks.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-simple-page
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Author-email: Thomas Leichtfuß <thomas.leichtfuss@posteo.de>
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Project-URL: Homepage, https://github.com/thomst/django-simple-page
|
|
7
|
+
Project-URL: Repository, https://github.com/thomst/django-simple-page
|
|
8
|
+
Project-URL: Documentation, https://github.com/thomst/django-simple-page#readme
|
|
9
|
+
Keywords: django,django-admin,cms,website
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 3.2
|
|
13
|
+
Classifier: Framework :: Django :: 4.0
|
|
14
|
+
Classifier: Framework :: Django :: 4.1
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.0
|
|
17
|
+
Classifier: Framework :: Django :: 5.1
|
|
18
|
+
Classifier: Framework :: Django :: 5.2
|
|
19
|
+
Classifier: Framework :: Django :: 6.0
|
|
20
|
+
Classifier: Environment :: Web Environment
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python
|
|
24
|
+
Classifier: Programming Language :: Python :: 3
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
+
Classifier: Topic :: Software Development
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENCE
|
|
31
|
+
Requires-Dist: Django>=3.2
|
|
32
|
+
Requires-Dist: django-mptt
|
|
33
|
+
Requires-Dist: django-model-utils
|
|
34
|
+
Requires-Dist: django-reorder_items_widget
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: Pillow; extra == "test"
|
|
37
|
+
Requires-Dist: coverage; extra == "test"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Welcome to django-simple-page
|
|
41
|
+
|
|
42
|
+
[](https://github.com/thomst/django-simple-page/actions/workflows/tests.yml)
|
|
43
|
+
[](https://coveralls.io/github/thomst/django-simple-page?branch=main)
|
|
44
|
+
[<img src="https://img.shields.io/badge/django-3.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-orange">](https://img.shields.io/badge/django-3.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-orange)
|
|
45
|
+
|
|
46
|
+
Django-simple-page is a cms buildkit for your website. The strength of this
|
|
47
|
+
project is its simplicity - using comprehensible yet powerful concepts. You get
|
|
48
|
+
the basic stuff, but retain all your freedom.
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- [github](https://github.com/django-simple-page/)
|
|
53
|
+
- [docs](https://thomst.github.io/django-simple-page/)
|
|
54
|
+
- [pypi](https://pypi.org/project/django-simple-page/)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Tree structured Pages**: By django-mptt.
|
|
60
|
+
- **Pages, regions and sections**: Assigning sections to regions on pages.
|
|
61
|
+
- **Custom rendering logic**: Each page or section can have its own renderer.
|
|
62
|
+
- **Simple yet powerful concept**: Everything can be customized by subclassing.
|
|
63
|
+
- **Admin backend integration**: Easy to use. Order elements via drag and drop.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Description
|
|
67
|
+
|
|
68
|
+
### Pages, regions and sections
|
|
69
|
+
|
|
70
|
+
You got a reliable database layout of pages and sections. Sections are
|
|
71
|
+
associated with regions on pages. Everything else is up to you. Sections could
|
|
72
|
+
be anything you want, from a simple content type like an article with title and
|
|
73
|
+
text body to a full featured gallery. You build what you need just by
|
|
74
|
+
subclassing the page and section model.
|
|
75
|
+
|
|
76
|
+
### Renderer
|
|
77
|
+
|
|
78
|
+
While there are default renderers for pages and sections which are probably
|
|
79
|
+
suitable for most use cases, you are free to completely adapt or overwrite them.
|
|
80
|
+
Each page or section can have its own renderer providing a specific rendering
|
|
81
|
+
logic. And each renderer can have its own Media class defining javascript or css
|
|
82
|
+
files. Those media assets are merged by the page renderer and be available as
|
|
83
|
+
`media` template variable.
|
|
84
|
+
|
|
85
|
+
### Admin integration
|
|
86
|
+
|
|
87
|
+
At least we provide a handy admin backend integration. Rearrange your pages by
|
|
88
|
+
drag and drop. Add sections to your page regions with inline formsets and
|
|
89
|
+
reorder them by just dragging them to their new position. It's simple and
|
|
90
|
+
sufficient.
|
|
91
|
+
|
|
92
|
+
### Summing-up
|
|
93
|
+
|
|
94
|
+
As you can see, everything is done by subclassing. While django-simple-page
|
|
95
|
+
giving you the basics to build your website, it is not taking any freedom from
|
|
96
|
+
you. You define your pages with regions, your sections as content, your
|
|
97
|
+
rendering logic with their media classes and put everything together like
|
|
98
|
+
building blocks.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENCE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
django_simple_page.egg-info/PKG-INFO
|
|
5
|
+
django_simple_page.egg-info/SOURCES.txt
|
|
6
|
+
django_simple_page.egg-info/dependency_links.txt
|
|
7
|
+
django_simple_page.egg-info/requires.txt
|
|
8
|
+
django_simple_page.egg-info/top_level.txt
|
|
9
|
+
simple_page/__init__.py
|
|
10
|
+
simple_page/__version__.py
|
|
11
|
+
simple_page/admin.py
|
|
12
|
+
simple_page/apps.py
|
|
13
|
+
simple_page/forms.py
|
|
14
|
+
simple_page/models.py
|
|
15
|
+
simple_page/renderer.py
|
|
16
|
+
simple_page/signals.py
|
|
17
|
+
simple_page/views.py
|
|
18
|
+
simple_page/templates/admin/simple_page/page/change_form.html
|
|
19
|
+
simple_page/templates/simple_page/menu.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simple_page
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-simple-page"
|
|
7
|
+
authors = [{name = "Thomas Leichtfuß", email = "thomas.leichtfuss@posteo.de"}]
|
|
8
|
+
description = ""
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
keywords = ["django", "django-admin", "cms", "website"]
|
|
12
|
+
license = { text = "BSD-3-Clause"}
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Framework :: Django",
|
|
16
|
+
"Framework :: Django :: 3.2",
|
|
17
|
+
"Framework :: Django :: 4.0",
|
|
18
|
+
"Framework :: Django :: 4.1",
|
|
19
|
+
"Framework :: Django :: 4.2",
|
|
20
|
+
"Framework :: Django :: 5.0",
|
|
21
|
+
"Framework :: Django :: 5.1",
|
|
22
|
+
"Framework :: Django :: 5.2",
|
|
23
|
+
"Framework :: Django :: 6.0",
|
|
24
|
+
"Environment :: Web Environment",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
30
|
+
"Topic :: Software Development",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"Django>=3.2",
|
|
35
|
+
"django-mptt",
|
|
36
|
+
"django-model-utils",
|
|
37
|
+
"django-reorder_items_widget",
|
|
38
|
+
]
|
|
39
|
+
dynamic = ["version"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/thomst/django-simple-page"
|
|
43
|
+
Repository = "https://github.com/thomst/django-simple-page"
|
|
44
|
+
Documentation = "https://github.com/thomst/django-simple-page#readme"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
packages = ["simple_page"]
|
|
48
|
+
include-package-data = false
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
simple_page = ["templates/**"]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.dynamic]
|
|
54
|
+
version = {attr = "simple_page.__version__.__version__"}
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
test = [
|
|
58
|
+
"Pillow",
|
|
59
|
+
"coverage",
|
|
60
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is django-simple-page
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
Django-simple-page is a cms buildkit for your website. The strength of this
|
|
6
|
+
project is its simplicity - using comprehensible yet powerful concepts. You get
|
|
7
|
+
the basic stuff, but retain all your freedom.
|
|
8
|
+
|
|
9
|
+
Features
|
|
10
|
+
========
|
|
11
|
+
|
|
12
|
+
- **Tree structured Pages**: By `django-mptt`_.
|
|
13
|
+
- **Pages and sections**: Assigning sections to regions on pages.
|
|
14
|
+
- **Renderer**: Each page or section can have its own renderer.
|
|
15
|
+
- **Simple yet powerful concept**: Everything can be customized by subclassing.
|
|
16
|
+
- **Admin backend integration**: Easy to use. Order elements via drag and drop.
|
|
17
|
+
|
|
18
|
+
.. _django-mptt: https://django-mptt.readthedocs.io/en/latest/
|
|
19
|
+
|
|
20
|
+
Basic Concept
|
|
21
|
+
=============
|
|
22
|
+
|
|
23
|
+
Pages and sections
|
|
24
|
+
------------------
|
|
25
|
+
|
|
26
|
+
You got a reliable database layout of :class:`pages <.models.Page>` and
|
|
27
|
+
:class:`sections <.models.Section>` objects. Sections are associated with
|
|
28
|
+
regions on pages. Everything else is up to you. Sections could be anything you
|
|
29
|
+
want, from a simple content type like an article with title and text body to a
|
|
30
|
+
full featured gallery. You build what you need just by subclassing the page and
|
|
31
|
+
section model.
|
|
32
|
+
|
|
33
|
+
Renderer
|
|
34
|
+
--------
|
|
35
|
+
|
|
36
|
+
While there are default :mod:`renderers <.renderer>` for pages and sections
|
|
37
|
+
which are probably suitable for most use cases, you are free to completely adapt
|
|
38
|
+
or overwrite them. Each page or section can have its own renderer providing a
|
|
39
|
+
specific rendering logic. And each renderer can have its own Media class
|
|
40
|
+
defining javascript or css files. Those media assets are merged by the page
|
|
41
|
+
renderer and be available as `media` template variable.
|
|
42
|
+
|
|
43
|
+
Summing-up
|
|
44
|
+
----------
|
|
45
|
+
|
|
46
|
+
As you can see, everything is done by subclassing. While django-simple-page
|
|
47
|
+
giving you the basics to build your website, it is not taking any freedom from
|
|
48
|
+
you. You define your pages with regions, your sections as content, your
|
|
49
|
+
rendering logic with their media classes and put everything together like
|
|
50
|
+
building blocks.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
Admin integration
|
|
54
|
+
=================
|
|
55
|
+
|
|
56
|
+
We provide a handy admin backend integration. Rearrange your pages by drag and
|
|
57
|
+
drop. Add sections to your page regions with inline formsets and reorder them by
|
|
58
|
+
just dragging them to their new position. It's simple and sufficient.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
Utils
|
|
62
|
+
=====
|
|
63
|
+
|
|
64
|
+
Menu template tag
|
|
65
|
+
-----------------
|
|
66
|
+
We provide an inclusion template tag to generate a tree based menu using nested
|
|
67
|
+
lists. Still you are free to build your own menu logic or customize the default
|
|
68
|
+
menu template to your needs. See :func:`~.templatetags.simple_page.menu` for
|
|
69
|
+
more details.
|
|
70
|
+
|
|
71
|
+
Page view
|
|
72
|
+
---------
|
|
73
|
+
A simple view function to render a page by its slug. Use it in your url
|
|
74
|
+
configuration. See :func:`~.views.page_view` for more details.
|
|
75
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This project uses the Semantic Versioning scheme in conjunction with PEP 0440:
|
|
3
|
+
<http://semver.org/>
|
|
4
|
+
<https://www.python.org/dev/peps/pep-0440>
|
|
5
|
+
|
|
6
|
+
Major versions introduce significant changes to the API, and backwards
|
|
7
|
+
compatibility is not guaranteed. Minor versions are for new features and other
|
|
8
|
+
backwards-compatible changes to the API. Patch versions are for bug fixes and
|
|
9
|
+
internal code changes that do not affect the API. Development versions are
|
|
10
|
+
incomplete states of a release .
|
|
11
|
+
|
|
12
|
+
Version 0.x should be considered a development version with an unstable API,
|
|
13
|
+
and backwards compatibility is not guaranteed for minor versions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The admin integration for django-simple-page is realized by a customized
|
|
3
|
+
:class:`page modeladmin <.PageAdmin>`. This modeladmin is registered for the
|
|
4
|
+
page model and will be used for all proxy page models. It provides the following
|
|
5
|
+
features:
|
|
6
|
+
|
|
7
|
+
- Let pages be orderable by drag and drop in the admin changelist view.
|
|
8
|
+
- Let the user choose the type of the page before rendering the page's add form.
|
|
9
|
+
- Set the initial value of the hidden page_type field in the page add form.
|
|
10
|
+
- Render an inline formset for each region of the page which allows to assign
|
|
11
|
+
sections to the region and rearrange them via drag and drop.
|
|
12
|
+
|
|
13
|
+
For your own concrete page models you should use :class:`~.admin.BasePageAdmin`
|
|
14
|
+
as a base class for your modeladmin. It will take care of rendering the inline
|
|
15
|
+
formsets for regions and setting the appropriate value for the hidden page_type
|
|
16
|
+
field.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from django.contrib import admin
|
|
20
|
+
from django.forms import HiddenInput
|
|
21
|
+
from django.utils.functional import cached_property
|
|
22
|
+
from django.utils.html import mark_safe
|
|
23
|
+
from django.urls import reverse
|
|
24
|
+
from django.contrib.contenttypes.models import ContentType
|
|
25
|
+
from django.utils.translation import gettext as _
|
|
26
|
+
from mptt.admin import DraggableMPTTAdmin
|
|
27
|
+
from .models import Page, PageSection
|
|
28
|
+
from .forms import ReorderRelationForm
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseRegionInline(admin.TabularInline):
|
|
32
|
+
"""
|
|
33
|
+
Inline for the :class:`~.models.PageSection` model. Each region of a page
|
|
34
|
+
gets its own inline table with sections belonging to that region. We use
|
|
35
|
+
the django-reorder-items-widget_ to make the sections orderable via drag and
|
|
36
|
+
drop within their region.
|
|
37
|
+
|
|
38
|
+
.. _django-reorder-items-widget: https://github.com/thomst/django-reorder-items-widget
|
|
39
|
+
"""
|
|
40
|
+
region_name = None
|
|
41
|
+
form = ReorderRelationForm
|
|
42
|
+
model = PageSection
|
|
43
|
+
extra = 1
|
|
44
|
+
fields = ("section", "index", "region")
|
|
45
|
+
|
|
46
|
+
def get_queryset(self, request):
|
|
47
|
+
qs = super().get_queryset(request)
|
|
48
|
+
return qs.filter(region=self.region_name)
|
|
49
|
+
|
|
50
|
+
class Media:
|
|
51
|
+
js = ["simple_page/formset_handlers.js"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GetPageModelMixin:
|
|
55
|
+
"""
|
|
56
|
+
A mixin to get the page model based on a page_type url query parameter or
|
|
57
|
+
the page_type field of the current page object.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@cached_property
|
|
61
|
+
def page_types(self):
|
|
62
|
+
"""
|
|
63
|
+
Return content types of all page models.
|
|
64
|
+
"""
|
|
65
|
+
exclude_apps = ["admin", "auth", "contenttypes", "sessions", "simple_page"]
|
|
66
|
+
cts = ContentType.objects.exclude(app_label__in=exclude_apps)
|
|
67
|
+
return [ct for ct in cts if ct.model_class() and issubclass(ct.model_class(), Page)]
|
|
68
|
+
|
|
69
|
+
def get_page_model(self, request, obj=None):
|
|
70
|
+
"""
|
|
71
|
+
Return the page model based on the request and object.
|
|
72
|
+
"""
|
|
73
|
+
if obj:
|
|
74
|
+
return obj.page_type.model_class()
|
|
75
|
+
elif 'page_type' in request.GET:
|
|
76
|
+
page_type_id = request.GET['page_type']
|
|
77
|
+
try:
|
|
78
|
+
page_type = [ct for ct in self.page_types if ct.id == int(page_type_id)][0]
|
|
79
|
+
except (IndexError, ValueError):
|
|
80
|
+
raise ValueError(f"Invalid page type id: {page_type_id}")
|
|
81
|
+
else:
|
|
82
|
+
return page_type.model_class()
|
|
83
|
+
else:
|
|
84
|
+
return self.model
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RenderPageRegionsMixin(GetPageModelMixin):
|
|
88
|
+
"""
|
|
89
|
+
Render a :class:`~.models.PageSection` inline formset for each region of the
|
|
90
|
+
page. Also make sure extra forms have the region's name as initial value for
|
|
91
|
+
the region form field.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def get_page_regions(self, request, obj):
|
|
95
|
+
return self.get_page_model(request, obj).get_regions()
|
|
96
|
+
|
|
97
|
+
def get_formset_kwargs(self, request, obj, inline, prefix):
|
|
98
|
+
kwargs = super().get_formset_kwargs(request, obj, inline, prefix)
|
|
99
|
+
if isinstance(inline, BaseRegionInline):
|
|
100
|
+
kwargs["initial"] = [
|
|
101
|
+
{"region": inline.region_name}
|
|
102
|
+
for i in range(inline.extra)
|
|
103
|
+
]
|
|
104
|
+
return kwargs
|
|
105
|
+
|
|
106
|
+
def get_inlines(self, request, obj):
|
|
107
|
+
inlines = list(super().get_inlines(request, obj))
|
|
108
|
+
regions = self.get_page_regions(request, obj)
|
|
109
|
+
for region, title in regions:
|
|
110
|
+
class_name = f"{region.capitalize()}Inline"
|
|
111
|
+
attrs = dict(
|
|
112
|
+
region_name=region,
|
|
113
|
+
verbose_name=title,
|
|
114
|
+
verbose_name_plural=title,
|
|
115
|
+
)
|
|
116
|
+
inlines.append(type(class_name, (BaseRegionInline,), attrs))
|
|
117
|
+
return inlines
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ChoosePageTypeMixin(GetPageModelMixin):
|
|
121
|
+
"""
|
|
122
|
+
Let the user choose the type of the page she wants to add. Therefore render
|
|
123
|
+
a simple list of add-links which either set the page_type url-query
|
|
124
|
+
parameter for proxy page models or link to the modeladmin changeform
|
|
125
|
+
for concrete page models.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
|
|
129
|
+
# Set the title of the change form based on the page type.
|
|
130
|
+
if add or change:
|
|
131
|
+
title = _("Add %s") if add else _("Change %s")
|
|
132
|
+
else:
|
|
133
|
+
title = _("View %s")
|
|
134
|
+
page_model = self.get_page_model(request, obj)
|
|
135
|
+
context["title"] = title % page_model._meta.verbose_name
|
|
136
|
+
return super().render_change_form(request, context, add, change, form_url, obj)
|
|
137
|
+
|
|
138
|
+
def add_view(self, request, form_url="", extra_context=None):
|
|
139
|
+
# Add page types to the context to render a list of add links for each
|
|
140
|
+
# page type. Using their content type id in the query string.
|
|
141
|
+
if "page_type" not in request.GET:
|
|
142
|
+
extra_context = extra_context or {}
|
|
143
|
+
extra_context["page_types"] = []
|
|
144
|
+
for ct in self.page_types:
|
|
145
|
+
name = ct.model_class()._meta.verbose_name
|
|
146
|
+
if ct.model_class()._meta.proxy:
|
|
147
|
+
url = f"?page_type={ct.id}"
|
|
148
|
+
else:
|
|
149
|
+
url = reverse(f"admin:{ct.app_label}_{ct.model}_add")
|
|
150
|
+
extra_context["page_types"].append((url, name))
|
|
151
|
+
return super().add_view(request, form_url, extra_context)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SetPageTypeMixin(GetPageModelMixin):
|
|
155
|
+
"""
|
|
156
|
+
Set the initial value of the hidden page_type field in changeforms when
|
|
157
|
+
adding a new page.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def get_form(self, request, obj=None, **kwargs):
|
|
161
|
+
form = super().get_form(request, obj, **kwargs)
|
|
162
|
+
page_model = self.get_page_model(request, obj)
|
|
163
|
+
form.base_fields["page_type"].initial = ContentType.objects.get_for_model(page_model)
|
|
164
|
+
form.base_fields["page_type"].widget = HiddenInput()
|
|
165
|
+
return form
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@admin.register(Page)
|
|
169
|
+
class PageAdmin(SetPageTypeMixin, ChoosePageTypeMixin, RenderPageRegionsMixin, DraggableMPTTAdmin):
|
|
170
|
+
"""
|
|
171
|
+
The modeladmin for all proxy page models. This modeladmin is already
|
|
172
|
+
registered for the page model. It provides the following features:
|
|
173
|
+
|
|
174
|
+
- Let pages be orderable by drag and drop in the admin changelist view.
|
|
175
|
+
- Let the user choose the type of the page before rendering the page's add
|
|
176
|
+
form.
|
|
177
|
+
- Set the initial value of the hidden page_type field in the page add form.
|
|
178
|
+
- Render an inline formset for each region of the page.
|
|
179
|
+
"""
|
|
180
|
+
list_display = ("tree_actions", "indented_title", "slug", "page_type", "view_page_link")
|
|
181
|
+
list_display_links=('indented_title',)
|
|
182
|
+
search_fields = ("title", "slug")
|
|
183
|
+
prepopulated_fields = {"slug": ("title",)}
|
|
184
|
+
list_filter = ("parent",)
|
|
185
|
+
|
|
186
|
+
def view_page_link(self, obj):
|
|
187
|
+
url = obj.get_absolute_url()
|
|
188
|
+
return mark_safe(f'<a href="{url}" target="_blank">View page</a>')
|
|
189
|
+
view_page_link.short_description = "View page"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class BasePageAdmin(SetPageTypeMixin, RenderPageRegionsMixin, admin.ModelAdmin):
|
|
193
|
+
"""
|
|
194
|
+
Base class for modeladmins for concrete page models. It provides the
|
|
195
|
+
following features:
|
|
196
|
+
|
|
197
|
+
- Set the initial value of the hidden page_type field in the page add form.
|
|
198
|
+
- Render an inline formset for each region of the page.
|
|
199
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.forms import ModelForm, HiddenInput
|
|
2
|
+
from reorder_items_widget import ReorderItemsWidget
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ReorderRelationForm(ModelForm):
|
|
6
|
+
"""
|
|
7
|
+
A ModelForm using the ReorderItemsWidget with an index field.
|
|
8
|
+
"""
|
|
9
|
+
class Meta:
|
|
10
|
+
widgets = {
|
|
11
|
+
'index': ReorderItemsWidget(attrs={'class': 'hidden'}),
|
|
12
|
+
'region': HiddenInput(),
|
|
13
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pages and sections are the basic building blocks of your website. Pages define
|
|
3
|
+
regions in which sections can be placed. And sections can be any kind of content
|
|
4
|
+
you want to see on your website.
|
|
5
|
+
|
|
6
|
+
Pages and sections are defined by subclassing the :class:`~.models.Page` and
|
|
7
|
+
:class:`~.models.Section` model::
|
|
8
|
+
|
|
9
|
+
from simple_page.models import Page, Section
|
|
10
|
+
|
|
11
|
+
class FancyPage(Page):
|
|
12
|
+
REGIONS = [
|
|
13
|
+
('main', 'Main Region'),
|
|
14
|
+
('sidebar', 'Sidebar'),
|
|
15
|
+
('footer', 'Footer'),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
proxy = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FancySection(Section):
|
|
23
|
+
title = models.CharField(max_length=255, blank=True)
|
|
24
|
+
text = models.TextField(blank=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
With those two models you are able to build a simple website.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from mptt.models import MPTTModel, TreeForeignKey
|
|
31
|
+
from model_utils.managers import InheritanceManager
|
|
32
|
+
|
|
33
|
+
from django.db import models
|
|
34
|
+
from django.urls import reverse
|
|
35
|
+
from django.contrib.contenttypes.models import ContentType
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Section(models.Model):
|
|
39
|
+
"""
|
|
40
|
+
Base model for what ever content you want to see on your website. It does
|
|
41
|
+
not has any fields by its own but can be equipped by sublcasses.
|
|
42
|
+
|
|
43
|
+
Sections are related to pages via a many-to-many relationship that holds the
|
|
44
|
+
region in which a section should be rendered and an index field to make the
|
|
45
|
+
sections orderable whithin that region.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
objects = InheritanceManager()
|
|
49
|
+
"""
|
|
50
|
+
We use the `InheritanceManager`_ to provide a simple api to access child
|
|
51
|
+
class objects.
|
|
52
|
+
|
|
53
|
+
.. _InheritanceManager: https://django-model-utils.readthedocs.io/en/latest/managers.html#inheritancemanager
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
if type(self) is Section:
|
|
58
|
+
child_self = self._meta.model.objects.get_subclass(id=self.id)
|
|
59
|
+
return f"{child_self._meta.verbose_name}: {child_self}"
|
|
60
|
+
else:
|
|
61
|
+
# FIXME: This leads to a recursive call if child_self is a Section
|
|
62
|
+
# as well. This happens when for any reason a section object has no
|
|
63
|
+
# child class.
|
|
64
|
+
return super().__str__()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Page(MPTTModel):
|
|
68
|
+
"""
|
|
69
|
+
Base model for all pages.
|
|
70
|
+
|
|
71
|
+
The only thing a subclass has to do is to setup its :attr:`regions
|
|
72
|
+
<.Page.REGIONS>`. Since the database layout is fully functional, you may
|
|
73
|
+
define your own page model as a proxy if you do not want to provide
|
|
74
|
+
additional fields.
|
|
75
|
+
|
|
76
|
+
Sections associated with a page are accessible by their region. Use the
|
|
77
|
+
region's name to get a queryset of sections belonging to that region.
|
|
78
|
+
|
|
79
|
+
The page model is tree structured by `django-mptt`_.
|
|
80
|
+
|
|
81
|
+
.. _django-mptt: https://django-mptt.readthedocs.io/en/latest/
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# FIXME: We should use REGIONS = None and raise a NotImplementedError. But
|
|
85
|
+
# tests are failing, since for any reason the get_regions method is called
|
|
86
|
+
# on a Page objects occacionally.
|
|
87
|
+
REGIONS = []
|
|
88
|
+
"""
|
|
89
|
+
REGIONS must be set by subclasses as a list of tuples holding the region's
|
|
90
|
+
name and its title. Something like::
|
|
91
|
+
|
|
92
|
+
REGIONS = [
|
|
93
|
+
('main', 'Main Region'),
|
|
94
|
+
('sidebar', 'Sidebar'),
|
|
95
|
+
('footer', 'Footer'),
|
|
96
|
+
]
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def get_regions(cls):
|
|
101
|
+
"""
|
|
102
|
+
Return the regions for this page. This method can be customized by child
|
|
103
|
+
classes to return different regions.
|
|
104
|
+
"""
|
|
105
|
+
return cls.REGIONS
|
|
106
|
+
|
|
107
|
+
def resolve_obj(self):
|
|
108
|
+
"""
|
|
109
|
+
Return the instance of the child class.
|
|
110
|
+
"""
|
|
111
|
+
model = self.page_type.model_class()
|
|
112
|
+
return model.objects.get(id=self.id)
|
|
113
|
+
|
|
114
|
+
title = models.CharField(max_length=255)
|
|
115
|
+
slug = models.SlugField(unique=True)
|
|
116
|
+
page_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
117
|
+
sections = models.ManyToManyField(
|
|
118
|
+
Section,
|
|
119
|
+
through="PageSection",
|
|
120
|
+
related_name="pages",
|
|
121
|
+
blank=True,
|
|
122
|
+
)
|
|
123
|
+
parent = TreeForeignKey(
|
|
124
|
+
"self",
|
|
125
|
+
null=True,
|
|
126
|
+
blank=True,
|
|
127
|
+
related_name="children",
|
|
128
|
+
on_delete=models.SET_NULL,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def get_absolute_url(self):
|
|
132
|
+
return reverse("page", kwargs={"slug": self.slug})
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return self.title
|
|
136
|
+
|
|
137
|
+
def __getattr__(self, name):
|
|
138
|
+
"""
|
|
139
|
+
If the attribute name is a region return its sections, otherwise raise
|
|
140
|
+
AttributeError.
|
|
141
|
+
"""
|
|
142
|
+
if name in [region for region, _ in self.get_regions()]:
|
|
143
|
+
sections = self.sections.filter(pagesection__region=name)
|
|
144
|
+
return sections.select_subclasses().order_by("pagesection__index")
|
|
145
|
+
else:
|
|
146
|
+
msg = f"{self.__class__.__name__} object has no attribute '{name}'"
|
|
147
|
+
raise AttributeError(msg)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class UpdateIndexesManager(models.Manager):
|
|
151
|
+
"""
|
|
152
|
+
Provide methods to set the index of an newly saved object and to fix
|
|
153
|
+
indexes of a set of items from which one was deleted.
|
|
154
|
+
"""
|
|
155
|
+
def set_index(self, obj):
|
|
156
|
+
"""
|
|
157
|
+
If an object is about to be added we give him the next higher
|
|
158
|
+
index. Call this method from a pre-save signal handler.
|
|
159
|
+
"""
|
|
160
|
+
items = self.filter(page=obj.page, region=obj.region)
|
|
161
|
+
max_index = items.aggregate(models.Max('index'))['index__max'] or 0
|
|
162
|
+
obj.index = max_index + 1
|
|
163
|
+
|
|
164
|
+
def update_indexes(self, obj):
|
|
165
|
+
"""
|
|
166
|
+
If an object was deleted fix the indexes of following objects. Call this
|
|
167
|
+
method from a post-delete signal handler.
|
|
168
|
+
"""
|
|
169
|
+
items = self.filter(page=obj.page, region=obj.region)
|
|
170
|
+
for item in items.filter(index__gt=obj.index):
|
|
171
|
+
item.index -= 1
|
|
172
|
+
item.save()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PageSection(models.Model):
|
|
176
|
+
"""
|
|
177
|
+
PageSection is the intermediate model for the many-to-many relationship
|
|
178
|
+
between pages and sections. It holds the region in which a section should be
|
|
179
|
+
rendered and an index field to make sections orderable whithin that region.
|
|
180
|
+
"""
|
|
181
|
+
objects = UpdateIndexesManager()
|
|
182
|
+
|
|
183
|
+
page = models.ForeignKey(Page, on_delete=models.CASCADE)
|
|
184
|
+
section = models.ForeignKey(Section, on_delete=models.CASCADE)
|
|
185
|
+
region = models.CharField('Region', max_length=255)
|
|
186
|
+
index = models.SmallIntegerField(blank=True)
|
|
187
|
+
|
|
188
|
+
class Meta:
|
|
189
|
+
ordering = ["page__id", "index"]
|
|
190
|
+
|
|
191
|
+
def __str__(self):
|
|
192
|
+
return f"{self.section}"
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
To build HTML for a page or section object a renderer class is used. While a
|
|
3
|
+
section renderer produces a html snippet representing the section object, a page
|
|
4
|
+
renderer provides a full html document for a page. Including all its sections.
|
|
5
|
+
|
|
6
|
+
Nevertheless, both renderers are based on the same concept, using the proven
|
|
7
|
+
triad of `get_template_name`, `get_context` and `render` methods.
|
|
8
|
+
|
|
9
|
+
There is a default renderer for pages as well as for sections. Which are
|
|
10
|
+
probably sufficient for most use cases. Still you are free to write your own
|
|
11
|
+
renderer classes and :func:`~.register` them for your page and section models.
|
|
12
|
+
The only thing a renderer class has to provide is a `render` method returning
|
|
13
|
+
valid HTML.
|
|
14
|
+
|
|
15
|
+
Renderer classes using django's :class:`~django.forms.MediaDefiningClass` as
|
|
16
|
+
metaclass. They can be equipped with a `Media` classes like django's forms and
|
|
17
|
+
widgets::
|
|
18
|
+
|
|
19
|
+
class FancySectionRenderer(SectionRenderer):
|
|
20
|
+
class Media:
|
|
21
|
+
css = dict(all=['fancy_section.css'])
|
|
22
|
+
js = ['fancy_section.js']
|
|
23
|
+
|
|
24
|
+
It is the responsibility of the page renderer to merge the media
|
|
25
|
+
definitions of all renderers involved and provide them as a `media` template
|
|
26
|
+
variable. For more details see :meth:`~.PageRenderer.get_media_assets`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from django.template.loader import get_template
|
|
31
|
+
from django.forms.widgets import MediaDefiningClass
|
|
32
|
+
from .models import Page
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
REGISTRY = dict()
|
|
36
|
+
|
|
37
|
+
def register(model_cls, renderer_cls=None, context=None):
|
|
38
|
+
"""
|
|
39
|
+
Register a :class:`renderer class <.BaseRenderer>` for a page or section
|
|
40
|
+
model. This function can also be used as a decorator for your renderer
|
|
41
|
+
class::
|
|
42
|
+
|
|
43
|
+
@renderer.register(FancyPage)
|
|
44
|
+
class FancyPageRenderer(PageRenderer):
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
:class:`Section renderer <.SectionRenderer>` can be applied context
|
|
48
|
+
specific. A context can be a page type, a region name or a tuple of page
|
|
49
|
+
type and region name::
|
|
50
|
+
|
|
51
|
+
@renderer.register(FancySection, context='main')
|
|
52
|
+
class MainRegionFancySectionRenderer(SectionRenderer):
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
or::
|
|
56
|
+
|
|
57
|
+
@renderer.register(FancySection, context=(FancyPage, 'main'))
|
|
58
|
+
class FancyPageMainRegionFancySectionRenderer(SectionRenderer):
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
This allows you to use different renderers depending on where a section
|
|
62
|
+
appears. See :func:`~.get_section_renderer` for more details about how a
|
|
63
|
+
renderer will be choosen.
|
|
64
|
+
|
|
65
|
+
:param model_cls: model to be rendered
|
|
66
|
+
:type model_cls: :class:`~.models.Page` or :class:`~.models.Section`
|
|
67
|
+
:param renderer_cls: renderer class
|
|
68
|
+
:type renderer_cls: :class:`~.PageRenderer` or :class:`~.SectionRenderer`
|
|
69
|
+
:param context: context where a section renderer should be applied
|
|
70
|
+
:type context: :class:`~.models.Page` or str or tuple of both, optional
|
|
71
|
+
"""
|
|
72
|
+
def _register(renderer_cls):
|
|
73
|
+
if issubclass(model_cls, Page):
|
|
74
|
+
REGISTRY[model_cls] = renderer_cls
|
|
75
|
+
else:
|
|
76
|
+
REGISTRY[model_cls] = REGISTRY.get(model_cls) or dict()
|
|
77
|
+
REGISTRY[model_cls][context] = renderer_cls
|
|
78
|
+
return model_cls
|
|
79
|
+
|
|
80
|
+
# Usage as function.
|
|
81
|
+
if renderer_cls:
|
|
82
|
+
_register(renderer_cls)
|
|
83
|
+
|
|
84
|
+
# Usage as decorator.
|
|
85
|
+
else:
|
|
86
|
+
return _register
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_page_renderer(page):
|
|
90
|
+
"""
|
|
91
|
+
Return the registered renderer for the page or :class:`~.PageRenderer`.
|
|
92
|
+
|
|
93
|
+
:param page: page instance to be rendered
|
|
94
|
+
:type page: :class:`~.models.Page`
|
|
95
|
+
:return: renderer class
|
|
96
|
+
:rtype: :class:`~.PageRenderer`
|
|
97
|
+
"""
|
|
98
|
+
return REGISTRY.get(type(page), PageRenderer)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_section_renderer(section, page=None, region=None):
|
|
102
|
+
"""
|
|
103
|
+
Return a renderer instance for the section.
|
|
104
|
+
|
|
105
|
+
We look for a registered renderer in this order:
|
|
106
|
+
|
|
107
|
+
* page-type and region specific
|
|
108
|
+
* region specific
|
|
109
|
+
* page-type specific
|
|
110
|
+
* neither page-type nor region specific
|
|
111
|
+
|
|
112
|
+
The first one found will be returned. Otherwise the
|
|
113
|
+
:class:`~.SectionRenderer` is used as fallback.
|
|
114
|
+
|
|
115
|
+
:param obj: section instance
|
|
116
|
+
:type obj: :class:`~.models.Section`
|
|
117
|
+
:param page: page the section will be rendered for
|
|
118
|
+
:type page: :class:`~.models.Page`
|
|
119
|
+
:param str region: region the section will be rendered in
|
|
120
|
+
:return: renderer class
|
|
121
|
+
:rtype: :class:`~.SectionRenderer`
|
|
122
|
+
"""
|
|
123
|
+
if type(section) in REGISTRY:
|
|
124
|
+
# One of these keys must have been used to register a renderer class.
|
|
125
|
+
for key in [(type(page), region), region, type(page), None]:
|
|
126
|
+
if key in REGISTRY[type(section)]:
|
|
127
|
+
return REGISTRY[type(section)][key]
|
|
128
|
+
else:
|
|
129
|
+
return SectionRenderer
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BaseRenderer(metaclass=MediaDefiningClass):
|
|
133
|
+
"""
|
|
134
|
+
Base renderer class.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
template_name = None
|
|
138
|
+
"""
|
|
139
|
+
Template name. Default is None. See :meth:`~.get_template_name`.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, obj, request=None, **kwargs):
|
|
143
|
+
"""
|
|
144
|
+
Initialize the renderer.
|
|
145
|
+
|
|
146
|
+
:param obj: object to be rendered
|
|
147
|
+
:type obj: :class:`~.models.Page` or :class:`~.models.Section`
|
|
148
|
+
:param request: request object (default: None)
|
|
149
|
+
:type request: :class:`~django.http.HttpRequest`
|
|
150
|
+
:param kwargs: Additional data as keyword arguments (default: dict())
|
|
151
|
+
"""
|
|
152
|
+
self.obj = obj
|
|
153
|
+
self.request = request
|
|
154
|
+
self.kwargs = kwargs
|
|
155
|
+
|
|
156
|
+
def render(self):
|
|
157
|
+
"""
|
|
158
|
+
Return the rendered HTML using the template and context returned by
|
|
159
|
+
:meth:`~.get_template_name` and :meth:`~.get_context` methods.
|
|
160
|
+
"""
|
|
161
|
+
template = get_template(self.get_template_name())
|
|
162
|
+
context = self.get_context()
|
|
163
|
+
return template.render(context)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SectionRenderer(BaseRenderer):
|
|
167
|
+
"""
|
|
168
|
+
Renderer for Section instances.
|
|
169
|
+
"""
|
|
170
|
+
# TODO: Use a get_template method instead.
|
|
171
|
+
def get_template_name(self):
|
|
172
|
+
"""
|
|
173
|
+
Return template name. If :attr:`~.template_name` is set it will be
|
|
174
|
+
returned. Otherwise the template name will be constructed as follows:
|
|
175
|
+
|
|
176
|
+
"sections/<section_class_name_in_snake_case>.html"
|
|
177
|
+
"""
|
|
178
|
+
if self.template_name:
|
|
179
|
+
return self.template_name
|
|
180
|
+
else:
|
|
181
|
+
cls_name = self.obj.__class__.__name__
|
|
182
|
+
template_name = re.sub(r'(?<!^)(?=[A-Z])', '_', cls_name).lower()
|
|
183
|
+
return f'sections/{template_name}.html'
|
|
184
|
+
|
|
185
|
+
def get_context(self):
|
|
186
|
+
"""
|
|
187
|
+
Build and return rendering context:
|
|
188
|
+
|
|
189
|
+
- `section`: section object
|
|
190
|
+
|
|
191
|
+
:return: rendering context
|
|
192
|
+
:rtype: dict
|
|
193
|
+
"""
|
|
194
|
+
context = self.kwargs.get('extra_context', dict())
|
|
195
|
+
context['section'] = self.obj
|
|
196
|
+
return context
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class PageRenderer(BaseRenderer):
|
|
200
|
+
"""
|
|
201
|
+
Renderer for Page instances.
|
|
202
|
+
"""
|
|
203
|
+
def get_template_name(self):
|
|
204
|
+
"""
|
|
205
|
+
Return template name. If :attr:`~.template_name` is set it will be
|
|
206
|
+
returned. Otherwise the template name will be constructed as follows:
|
|
207
|
+
|
|
208
|
+
"pages/<page_class_name_in_snake_case>.html"
|
|
209
|
+
"""
|
|
210
|
+
if self.template_name:
|
|
211
|
+
return self.template_name
|
|
212
|
+
else:
|
|
213
|
+
cls_name = self.obj.__class__.__name__
|
|
214
|
+
template_name = re.sub(r'(?<!^)(?=[A-Z])', '_', cls_name).lower()
|
|
215
|
+
return f'pages/{template_name}.html'
|
|
216
|
+
|
|
217
|
+
def get_section_data(self, section, region):
|
|
218
|
+
"""
|
|
219
|
+
Build and return a dictionary holding the section's data:
|
|
220
|
+
|
|
221
|
+
- `obj`: section object itself
|
|
222
|
+
- `html`: section's html build by the renderer returned by :func:`~.get_section_renderer`
|
|
223
|
+
|
|
224
|
+
:param section: section object
|
|
225
|
+
:type section: :class:`~.models.Section`
|
|
226
|
+
:param str region: region name
|
|
227
|
+
:return: section data holding the section object and the rendered html
|
|
228
|
+
:rtype: dict
|
|
229
|
+
"""
|
|
230
|
+
renderer_cls = get_section_renderer(section, self.obj, region)
|
|
231
|
+
renderer = renderer_cls(section, self.request, **self.kwargs)
|
|
232
|
+
return dict(
|
|
233
|
+
obj=section,
|
|
234
|
+
html=renderer.render()
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def get_region_data(self, region, title):
|
|
238
|
+
"""
|
|
239
|
+
Build and return a dictionary holding the region's data:
|
|
240
|
+
|
|
241
|
+
- `name`: region name
|
|
242
|
+
- `title`: region title
|
|
243
|
+
- `sections`: list of section data build by :meth:`~.get_section_data`
|
|
244
|
+
|
|
245
|
+
:param str region: region name
|
|
246
|
+
:param str tilte: region title
|
|
247
|
+
:return: region data holding title, name and sections for this region
|
|
248
|
+
:rtype: dict
|
|
249
|
+
"""
|
|
250
|
+
region_data = {'title': title, 'name': region, 'sections': []}
|
|
251
|
+
for section in getattr(self.obj, region):
|
|
252
|
+
section_data = self.get_section_data(section, region)
|
|
253
|
+
region_data['sections'].append(section_data)
|
|
254
|
+
return region_data
|
|
255
|
+
|
|
256
|
+
def get_media_assets(self):
|
|
257
|
+
"""
|
|
258
|
+
Merge media definitions of the page's and all sections' renderers.
|
|
259
|
+
Return them as string.
|
|
260
|
+
|
|
261
|
+
:return str: merged media assets
|
|
262
|
+
"""
|
|
263
|
+
media = get_page_renderer(self.obj)(self.obj).media
|
|
264
|
+
for region, _ in self.obj.get_regions():
|
|
265
|
+
for section in getattr(self.obj, region):
|
|
266
|
+
section_renderer = get_section_renderer(section, self.obj, region)
|
|
267
|
+
media += section_renderer(section).media
|
|
268
|
+
return str(media)
|
|
269
|
+
|
|
270
|
+
def get_context(self):
|
|
271
|
+
"""
|
|
272
|
+
Build rendering context:
|
|
273
|
+
|
|
274
|
+
- `page`: page object
|
|
275
|
+
- `media`: media assets build by :meth:`~.get_media_assets`
|
|
276
|
+
- `regions`: mapping of region names to their data build by
|
|
277
|
+
:meth:`~.get_region_data`
|
|
278
|
+
|
|
279
|
+
As a shortcut each region data will also be added using the region's
|
|
280
|
+
name as an own context variable.
|
|
281
|
+
|
|
282
|
+
:return: rendering context
|
|
283
|
+
:rtype: dict
|
|
284
|
+
"""
|
|
285
|
+
# Add regions, sections and media to the context.
|
|
286
|
+
context = self.kwargs.get('extra_context', dict())
|
|
287
|
+
context['page'] = self.obj
|
|
288
|
+
context['media'] = self.get_media_assets()
|
|
289
|
+
context['regions'] = dict()
|
|
290
|
+
for region, title in self.obj.get_regions():
|
|
291
|
+
context[region] = self.get_region_data(region, title)
|
|
292
|
+
context['regions'][region] = context[region]
|
|
293
|
+
|
|
294
|
+
return context
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.db.models.signals import pre_save, post_delete
|
|
2
|
+
from django.dispatch import receiver
|
|
3
|
+
from .models import PageSection
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@receiver(pre_save, sender=PageSection)
|
|
7
|
+
def pre_save_page_section(sender, instance, **kwargs):
|
|
8
|
+
# Ensure instance was added and not changed.
|
|
9
|
+
if instance.pk is None:
|
|
10
|
+
PageSection.objects.set_index(instance)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@receiver(post_delete, sender=PageSection)
|
|
14
|
+
def post_delete_page_section(sender, instance, **kwargs):
|
|
15
|
+
PageSection.objects.update_indexes(instance)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{% extends "admin/change_form.html" %}
|
|
2
|
+
{% load i18n static %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
{% if page_types %}
|
|
6
|
+
<h2>{% translate "Choose page type" %}</h2>
|
|
7
|
+
<ul>
|
|
8
|
+
{% for url, name in page_types %}
|
|
9
|
+
<li>
|
|
10
|
+
<a href="{{ url }}" class="addlink" role="button">
|
|
11
|
+
{% translate "Add" %} {{ name }}
|
|
12
|
+
</a>
|
|
13
|
+
</li>
|
|
14
|
+
{% endfor %}
|
|
15
|
+
</ul>
|
|
16
|
+
{% else %}
|
|
17
|
+
{{ block.super }}
|
|
18
|
+
{% endif %}
|
|
19
|
+
{% endblock %}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% load simple_page mptt_tags %}
|
|
2
|
+
<ul class="nav-level-1">
|
|
3
|
+
{% if root %}
|
|
4
|
+
<li{% if page.is_root_node %} class="active"{% endif %}>
|
|
5
|
+
<a href="{{ root.get_absolute_url }}">{{ root.title }}</a>
|
|
6
|
+
</li>
|
|
7
|
+
{% endif %}
|
|
8
|
+
{% recursetree nodes %}
|
|
9
|
+
<li{% if page|is_active:node %} class="active"{% endif %}>
|
|
10
|
+
<a href="{{ node.get_absolute_url }}">{{ node.title }}</a>
|
|
11
|
+
{% if not node.is_leaf_node and not node|level > max_level%}
|
|
12
|
+
<ul class="nav-level-{{ node|level }}">
|
|
13
|
+
{{ children }}
|
|
14
|
+
</ul>
|
|
15
|
+
{% endif %}
|
|
16
|
+
</li>
|
|
17
|
+
{% endrecursetree %}
|
|
18
|
+
</ul>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.shortcuts import get_object_or_404
|
|
2
|
+
from django.http import HttpResponse
|
|
3
|
+
from .models import Page
|
|
4
|
+
from .renderer import get_page_renderer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def page_view(request, slug, **kwargs):
|
|
8
|
+
"""
|
|
9
|
+
Simple view function for pages. Get the page by its slug, find the right renderer
|
|
10
|
+
for it and return an HTTP response with the rendered page.
|
|
11
|
+
|
|
12
|
+
:param request: HTTP request
|
|
13
|
+
:type request: :class:`~django.http.HttpRequest`
|
|
14
|
+
:param str slug: slug of the page to be rendered
|
|
15
|
+
:param kwargs: Additional data as keyword arguments (default empty dict)
|
|
16
|
+
:return: HTTP response with the rendered page
|
|
17
|
+
:rtype: :class:`~django.http.HttpResponse`
|
|
18
|
+
:raises Http404: if no page with the given slug exists
|
|
19
|
+
"""
|
|
20
|
+
page = get_object_or_404(Page, slug=slug).resolve_obj()
|
|
21
|
+
renderer_cls = get_page_renderer(page)
|
|
22
|
+
return HttpResponse(renderer_cls(page, request, **kwargs).render())
|