maykin-common 0.1.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.
Files changed (29) hide show
  1. maykin_common-0.1.0/LICENSE +7 -0
  2. maykin_common-0.1.0/MANIFEST.in +9 -0
  3. maykin_common-0.1.0/PKG-INFO +149 -0
  4. maykin_common-0.1.0/README.rst +96 -0
  5. maykin_common-0.1.0/maykin_common/__init__.py +0 -0
  6. maykin_common-0.1.0/maykin_common/apps.py +9 -0
  7. maykin_common-0.1.0/maykin_common/checks.py +78 -0
  8. maykin_common-0.1.0/maykin_common/context_processors.py +18 -0
  9. maykin_common-0.1.0/maykin_common/django_two_factor_auth.py +20 -0
  10. maykin_common-0.1.0/maykin_common/management/__init__.py +0 -0
  11. maykin_common-0.1.0/maykin_common/management/commands/__init__.py +0 -0
  12. maykin_common-0.1.0/maykin_common/management/commands/clear_cache.py +16 -0
  13. maykin_common-0.1.0/maykin_common/migration_operations.py +61 -0
  14. maykin_common-0.1.0/maykin_common/mixins.py +92 -0
  15. maykin_common-0.1.0/maykin_common/pdf.py +120 -0
  16. maykin_common-0.1.0/maykin_common/py.typed +0 -0
  17. maykin_common-0.1.0/maykin_common/settings.py +34 -0
  18. maykin_common-0.1.0/maykin_common/static/maykin_common/css/version.css +17 -0
  19. maykin_common-0.1.0/maykin_common/templates/maykin_common/includes/version_info.html +12 -0
  20. maykin_common-0.1.0/maykin_common/templatetags/__init__.py +0 -0
  21. maykin_common-0.1.0/maykin_common/templatetags/maykin_common.py +46 -0
  22. maykin_common-0.1.0/maykin_common/views.py +25 -0
  23. maykin_common-0.1.0/maykin_common.egg-info/PKG-INFO +149 -0
  24. maykin_common-0.1.0/maykin_common.egg-info/SOURCES.txt +27 -0
  25. maykin_common-0.1.0/maykin_common.egg-info/dependency_links.txt +1 -0
  26. maykin_common-0.1.0/maykin_common.egg-info/requires.txt +25 -0
  27. maykin_common-0.1.0/maykin_common.egg-info/top_level.txt +1 -0
  28. maykin_common-0.1.0/pyproject.toml +137 -0
  29. maykin_common-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Maykin Media
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ include *.rst
2
+ include LICENSE
3
+ include maykin_common/py.typed
4
+ recursive-include maykin_common *.html
5
+ recursive-include maykin_common *.css
6
+ recursive-include maykin_common *.txt
7
+ recursive-include maykin_common *.po
8
+ global-exclude __pycache__
9
+ global-exclude *.py[co]
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: maykin-common
3
+ Version: 0.1.0
4
+ Summary: Re-usable utilities for Maykin Django projects, centrally maintained instead of in default-project.
5
+ Author-email: Maykin Media <support@maykinmedia.nl>
6
+ License: Copyright 2025 Maykin Media
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13
+
14
+ Project-URL: Homepage, https://github.com/maykinmedia/django-common
15
+ Project-URL: Documentation, http://maykin-django-common.readthedocs.io/en/latest/
16
+ Project-URL: Bug Tracker, https://github.com/maykinmedia/django-common/issues
17
+ Project-URL: Source Code, https://github.com/maykinmedia/django-common
18
+ Keywords: django,maykin,boilerplate
19
+ Classifier: Development Status :: 3 - Alpha
20
+ Classifier: Framework :: Django
21
+ Classifier: Framework :: Django :: 4.2
22
+ Classifier: Framework :: Django :: 5.2
23
+ Classifier: Intended Audience :: Developers
24
+ Classifier: Operating System :: Unix
25
+ Classifier: Operating System :: MacOS
26
+ Classifier: Operating System :: Microsoft :: Windows
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Requires-Python: >=3.12
31
+ Description-Content-Type: text/x-rst
32
+ License-File: LICENSE
33
+ Requires-Dist: django>=4.2
34
+ Provides-Extra: tests
35
+ Requires-Dist: pytest; extra == "tests"
36
+ Requires-Dist: pytest-django; extra == "tests"
37
+ Requires-Dist: pytest-cov; extra == "tests"
38
+ Requires-Dist: tox; extra == "tests"
39
+ Requires-Dist: ruff; extra == "tests"
40
+ Provides-Extra: docs
41
+ Requires-Dist: sphinx; extra == "docs"
42
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
43
+ Provides-Extra: release
44
+ Requires-Dist: bump-my-version; extra == "release"
45
+ Provides-Extra: pdf
46
+ Requires-Dist: weasyprint; extra == "pdf"
47
+ Provides-Extra: mfa
48
+ Requires-Dist: django-admin-index; extra == "mfa"
49
+ Requires-Dist: maykin-2fa; extra == "mfa"
50
+ Provides-Extra: axes
51
+ Requires-Dist: django-axes; extra == "axes"
52
+ Dynamic: license-file
53
+
54
+
55
+
56
+ Welcome to maykin-common's documentation!
57
+ =================================================
58
+
59
+ :Version: 0.1.0
60
+ :Source: https://github.com/maykinmedia/django-common
61
+ :Keywords: ``<keywords>``
62
+ :PythonVersion: 3.12
63
+
64
+ |build-status| |code-quality| |ruff| |coverage| |docs|
65
+
66
+ |python-versions| |django-versions| |pypi-version|
67
+
68
+
69
+ <One liner describing the project>
70
+
71
+ .. contents::
72
+
73
+ .. section-numbering::
74
+
75
+ Features
76
+ ========
77
+
78
+ * ...
79
+ * ...
80
+
81
+ Installation
82
+ ============
83
+
84
+ Requirements
85
+ ------------
86
+
87
+ * Python 3.12 or above
88
+ * Django 4.2 or newer
89
+
90
+
91
+ Install
92
+ -------
93
+
94
+ .. code-block:: bash
95
+
96
+ pip install maykin-common
97
+
98
+
99
+ Usage
100
+ =====
101
+
102
+ <document or refer to docs>
103
+
104
+ Local development
105
+ =================
106
+
107
+ To install and develop the library locally, use::
108
+
109
+ .. code-block:: bash
110
+
111
+ pip install -e .[tests,coverage,docs,release]
112
+
113
+ When running management commands via ``django-admin``, make sure to add the root
114
+ directory to the python path (or use ``python -m django <command>``):
115
+
116
+ .. code-block:: bash
117
+
118
+ export PYTHONPATH=. DJANGO_SETTINGS_MODULE=testapp.settings
119
+ django-admin check
120
+ # or other commands like:
121
+ # django-admin makemessages -l nl
122
+
123
+
124
+ .. |build-status| image:: https://github.com/maykinmedia/django-common/workflows/Run%20CI/badge.svg
125
+ :alt: Build status
126
+ :target: https://github.com/maykinmedia/django-common/actions?query=workflow%3A%22Run+CI%22
127
+
128
+ .. |code-quality| image:: https://github.com/maykinmedia/django-common/workflows/Code%20quality%20checks/badge.svg
129
+ :alt: Code quality checks
130
+ :target: https://github.com/maykinmedia/django-common/actions?query=workflow%3A%22Code+quality+checks%22
131
+
132
+ .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
133
+ :target: https://github.com/astral-sh/ruff
134
+ :alt: Ruff
135
+
136
+ .. |coverage| image:: https://codecov.io/gh/maykinmedia/django-common/branch/main/graph/badge.svg
137
+ :target: https://codecov.io/gh/maykinmedia/django-common
138
+ :alt: Coverage status
139
+
140
+ .. |docs| image:: https://readthedocs.org/projects/django-common/badge/?version=latest
141
+ :target: https://maykin-django-common.readthedocs.io/en/latest/?badge=latest
142
+ :alt: Documentation Status
143
+
144
+ .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-common.svg
145
+
146
+ .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-common.svg
147
+
148
+ .. |pypi-version| image:: https://img.shields.io/pypi/v/django-common.svg
149
+ :target: https://pypi.org/project/django-common/
@@ -0,0 +1,96 @@
1
+
2
+
3
+ Welcome to maykin-common's documentation!
4
+ =================================================
5
+
6
+ :Version: 0.1.0
7
+ :Source: https://github.com/maykinmedia/django-common
8
+ :Keywords: ``<keywords>``
9
+ :PythonVersion: 3.12
10
+
11
+ |build-status| |code-quality| |ruff| |coverage| |docs|
12
+
13
+ |python-versions| |django-versions| |pypi-version|
14
+
15
+
16
+ <One liner describing the project>
17
+
18
+ .. contents::
19
+
20
+ .. section-numbering::
21
+
22
+ Features
23
+ ========
24
+
25
+ * ...
26
+ * ...
27
+
28
+ Installation
29
+ ============
30
+
31
+ Requirements
32
+ ------------
33
+
34
+ * Python 3.12 or above
35
+ * Django 4.2 or newer
36
+
37
+
38
+ Install
39
+ -------
40
+
41
+ .. code-block:: bash
42
+
43
+ pip install maykin-common
44
+
45
+
46
+ Usage
47
+ =====
48
+
49
+ <document or refer to docs>
50
+
51
+ Local development
52
+ =================
53
+
54
+ To install and develop the library locally, use::
55
+
56
+ .. code-block:: bash
57
+
58
+ pip install -e .[tests,coverage,docs,release]
59
+
60
+ When running management commands via ``django-admin``, make sure to add the root
61
+ directory to the python path (or use ``python -m django <command>``):
62
+
63
+ .. code-block:: bash
64
+
65
+ export PYTHONPATH=. DJANGO_SETTINGS_MODULE=testapp.settings
66
+ django-admin check
67
+ # or other commands like:
68
+ # django-admin makemessages -l nl
69
+
70
+
71
+ .. |build-status| image:: https://github.com/maykinmedia/django-common/workflows/Run%20CI/badge.svg
72
+ :alt: Build status
73
+ :target: https://github.com/maykinmedia/django-common/actions?query=workflow%3A%22Run+CI%22
74
+
75
+ .. |code-quality| image:: https://github.com/maykinmedia/django-common/workflows/Code%20quality%20checks/badge.svg
76
+ :alt: Code quality checks
77
+ :target: https://github.com/maykinmedia/django-common/actions?query=workflow%3A%22Code+quality+checks%22
78
+
79
+ .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
80
+ :target: https://github.com/astral-sh/ruff
81
+ :alt: Ruff
82
+
83
+ .. |coverage| image:: https://codecov.io/gh/maykinmedia/django-common/branch/main/graph/badge.svg
84
+ :target: https://codecov.io/gh/maykinmedia/django-common
85
+ :alt: Coverage status
86
+
87
+ .. |docs| image:: https://readthedocs.org/projects/django-common/badge/?version=latest
88
+ :target: https://maykin-django-common.readthedocs.io/en/latest/?badge=latest
89
+ :alt: Documentation Status
90
+
91
+ .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-common.svg
92
+
93
+ .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-common.svg
94
+
95
+ .. |pypi-version| image:: https://img.shields.io/pypi/v/django-common.svg
96
+ :target: https://pypi.org/project/django-common/
File without changes
@@ -0,0 +1,9 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class MaykinCommonConfig(AppConfig):
5
+ name = "maykin_common"
6
+
7
+ def ready(self):
8
+ from . import checks # noqa
9
+ from . import settings # noqa
@@ -0,0 +1,78 @@
1
+ import os
2
+
3
+ from django.conf import settings
4
+ from django.core.checks import Error, Warning, register
5
+ from django.forms import ModelForm
6
+
7
+
8
+ def get_subclasses(cls):
9
+ for subclass in cls.__subclasses__():
10
+ yield from get_subclasses(subclass)
11
+ yield subclass
12
+
13
+
14
+ @register()
15
+ def check_modelform_exclude(app_configs, **kwargs):
16
+ """
17
+ Check that ModelForms use Meta.fields instead of Meta.exclude.
18
+
19
+ ModelForm.Meta.exclude is dangerous because it doesn't protect against
20
+ fields that are added later. Explicit white-listing is safer and prevents
21
+ bugs such as IMA #645.
22
+
23
+ This check piggy-backs on all form modules to be imported during Django
24
+ startup. It won't cover forms that are defined on the fly such as in
25
+ formset factories.
26
+ """
27
+ errors = []
28
+
29
+ for form in get_subclasses(ModelForm):
30
+ # ok, fields is defined
31
+ if form._meta.fields or getattr(form.Meta, "fields", None):
32
+ continue
33
+
34
+ # no `.fields` defined, so scream loud enough to prevent this
35
+ errors.append(
36
+ Error(
37
+ f"ModelForm {form.__module__}.{form.__name__} with Meta.exclude "
38
+ "detected, this is a bad practice",
39
+ hint="Use ModelForm.Meta.fields instead",
40
+ obj=form,
41
+ id="maykin.E001",
42
+ )
43
+ )
44
+
45
+ return errors
46
+
47
+
48
+ @register
49
+ def check_missing_init_files(app_configs, **kwargs):
50
+ """
51
+ Check that all packages have __init__.py files.
52
+
53
+ If they don't, the code will still run, but tests aren't picked up by the
54
+ test runner, for example.
55
+ """
56
+ errors = []
57
+
58
+ for dirpath, _, filenames in os.walk(settings.DJANGO_PROJECT_DIR):
59
+ dirname = os.path.split(dirpath)[1]
60
+ if dirname == "__pycache__":
61
+ continue
62
+
63
+ if "__init__.py" in filenames:
64
+ continue
65
+
66
+ extensions = [os.path.splitext(fn)[1] for fn in filenames]
67
+ if ".py" not in extensions:
68
+ continue
69
+
70
+ errors.append(
71
+ Warning(
72
+ f"Directory {dirpath} does not contain an `__init__.py` file {dirname}",
73
+ hint="Consider adding this module to make sure tests are picked up",
74
+ id="maykin.W001",
75
+ )
76
+ )
77
+
78
+ return errors
@@ -0,0 +1,18 @@
1
+ from django.conf import settings as django_settings
2
+
3
+
4
+ def settings(request):
5
+ """ """
6
+ public_settings = (
7
+ "GOOGLE_ANALYTICS_ID",
8
+ "PROJECT_NAME",
9
+ )
10
+
11
+ context = {
12
+ "settings": {k: getattr(django_settings, k, None) for k in public_settings},
13
+ }
14
+
15
+ if hasattr(django_settings, "SENTRY_CONFIG"):
16
+ context.update(dsn=django_settings.SENTRY_CONFIG.get("public_dsn", ""))
17
+
18
+ return context
@@ -0,0 +1,20 @@
1
+ from django_admin_index.utils import (
2
+ should_display_dropdown_menu as default_should_display_dropdown_menu,
3
+ )
4
+
5
+
6
+ def should_display_dropdown_menu(request) -> bool:
7
+ """
8
+ Overrides the default `should_display_dropdown_menu` from `django_admin_index.utils`
9
+ to:
10
+ - not display the dropdown in `maykin_2fa` views.
11
+ - only display the dropdown to verified users.
12
+ """
13
+ default = default_should_display_dropdown_menu(request)
14
+
15
+ # never display the dropdown in two-factor admin views
16
+ if request.resolver_match.view_name.startswith("maykin_2fa:"):
17
+ return False
18
+
19
+ # do not display the dropdown until the user is verified.
20
+ return default and request.user.is_verified()
@@ -0,0 +1,16 @@
1
+ from django.core.cache import DEFAULT_CACHE_ALIAS, caches
2
+ from django.core.management import BaseCommand
3
+ from django.utils.translation import gettext as _
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = "Clears the Django cache"
8
+
9
+ def add_arguments(self, parser):
10
+ parser.add_argument("--alias", help=_("Clear given cache only"))
11
+
12
+ def handle(self, *args, **options):
13
+ alias = options["alias"]
14
+ cache = caches[alias] if alias else caches[DEFAULT_CACHE_ALIAS]
15
+
16
+ cache.clear()
@@ -0,0 +1,61 @@
1
+ from django.db import migrations, router
2
+
3
+ RESET_SQL = """
4
+ SELECT 'SELECT SETVAL(' ||
5
+ quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
6
+ ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
7
+ quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
8
+ FROM pg_class AS S,
9
+ pg_depend AS D,
10
+ pg_class AS T,
11
+ pg_attribute AS C,
12
+ pg_tables AS PGT
13
+ WHERE S.relkind = 'S'
14
+ AND S.oid = D.objid
15
+ AND D.refobjid = T.oid
16
+ AND D.refobjid = C.attrelid
17
+ AND D.refobjsubid = C.attnum
18
+ AND T.relname = PGT.tablename
19
+ ORDER BY S.relname;
20
+ """
21
+
22
+
23
+ class ResetSequences(migrations.RunSQL):
24
+ """
25
+ Run the reset_sequences SQL.
26
+
27
+ Resetting the Postgres sequences makes sure you don't get Integrity Errors
28
+ when creating objects because of failing unique constraints. This happens
29
+ when you create records explicitly with PKs, bypassing the database PK
30
+ generation. Resetting the sequences makes sure the sequences are aware of
31
+ the used PKs.
32
+
33
+ Usage:
34
+
35
+ >>> from maykin_common.migration_operations import ResetSequences
36
+ >>> class Migration(migrations.Migration):
37
+ ... dependencies = (...)
38
+ ... operations = [
39
+ ... ResetSequences(),
40
+ ... ]
41
+ """
42
+
43
+ reversible = True
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ super().__init__(None, *args, **kwargs)
47
+
48
+ def database_forwards(self, app_label, schema_editor, from_state, to_state) -> None:
49
+ if router.allow_migrate(
50
+ schema_editor.connection.alias, app_label, **self.hints
51
+ ):
52
+ with schema_editor.connection.cursor() as cursor:
53
+ cursor.execute(RESET_SQL)
54
+ rows = cursor.fetchall()
55
+
56
+ sql = "\n".join(x[0] for x in rows)
57
+
58
+ self._run_sql(schema_editor, sql)
59
+
60
+ def database_backwards(self, *args, **kwargs) -> None:
61
+ pass
@@ -0,0 +1,92 @@
1
+ from time import time
2
+
3
+ from django.core.cache import caches
4
+ from django.core.exceptions import PermissionDenied
5
+ from django.http import HttpRequest
6
+
7
+ from axes.helpers import get_client_ip_address
8
+
9
+
10
+ class ThrottleMixin:
11
+ """
12
+ A very simple throttling implementation with, hopefully, sane defaults.
13
+
14
+ You can specifiy the amount of visits (throttle_visits) a view can get,
15
+ for a specific period (in seconds) throttle_period.
16
+ """
17
+
18
+ # n visits per period (in seconds)
19
+ throttle_visits = 100
20
+ throttle_period = 60**2 # in seconds
21
+ throttle_403 = True
22
+ throttle_name = "default"
23
+
24
+ # get and options should always be fast. By default
25
+ # do not throttle them.
26
+ throttle_methods = ["post", "put", "patch", "delete", "head", "trace"]
27
+
28
+ request: HttpRequest
29
+
30
+ def get_throttle_cache(self):
31
+ return caches["default"]
32
+
33
+ def get_throttle_identifier(self):
34
+ user = getattr(self, "user_cache", self.request.user)
35
+ return str(user.id)
36
+
37
+ def create_throttle_key(self):
38
+ """
39
+ :rtype string Use as key to save the last access
40
+ """
41
+
42
+ return (
43
+ f"throttling_{self.get_throttle_identifier()}_"
44
+ f"{self.throttle_name}_{self.get_throttle_window()}"
45
+ )
46
+
47
+ def get_throttle_window(self):
48
+ """
49
+ round down to the throttle_period, which is then used to create the key.
50
+ """
51
+ current_time = int(time())
52
+ return current_time - (current_time % self.throttle_period)
53
+
54
+ def get_visits_in_window(self):
55
+ cache = self.get_throttle_cache()
56
+ key = self.create_throttle_key()
57
+
58
+ initial_visits = 1
59
+ stored = cache.add(key, initial_visits, self.throttle_period)
60
+ if stored:
61
+ visits = initial_visits
62
+ else:
63
+ try:
64
+ visits = cache.incr(key)
65
+ except ValueError:
66
+ visits = initial_visits
67
+ return visits
68
+
69
+ def should_be_throttled(self):
70
+ if self.throttle_methods == "all":
71
+ return True
72
+ return self.request.method.lower() in self.throttle_methods
73
+
74
+ def dispatch(self, request, *args, **kwargs):
75
+ if self.throttle_403:
76
+ if (
77
+ self.should_be_throttled()
78
+ and self.get_visits_in_window() > self.throttle_visits
79
+ ):
80
+ raise PermissionDenied
81
+
82
+ return super().dispatch(request, *args, **kwargs)
83
+
84
+
85
+ class IPThrottleMixin(ThrottleMixin):
86
+ """
87
+ Same behavior as ThrottleMixin except it limits the amount of tries
88
+ per IP-address a user can access a certain view.
89
+ """
90
+
91
+ def get_throttle_identifier(self):
92
+ return get_client_ip_address(self.request)
@@ -0,0 +1,120 @@
1
+ """
2
+ Utilities for PDF rendering from HTML using WeasyPrint.
3
+
4
+ Note that you need to add https://pypi.org/project/weasyprint/ to your dependencies
5
+ if you want to make use of HTML-to-PDF rendering. This is not included by default as
6
+ it's quite heavy and requires OS-level dependencies.
7
+
8
+ This module exposes the public function :func:`render_to_pdf` which renders a template
9
+ with a context into a PDF document (bytes output). You can use "external" stylesheets
10
+ in these templates, and they will be resolved through django's staticfiles machinery
11
+ by the custom :class:`UrlFetcher`.
12
+ """
13
+
14
+ import logging
15
+ import mimetypes
16
+ from io import BytesIO
17
+ from pathlib import PurePosixPath
18
+ from urllib.parse import urljoin, urlparse
19
+
20
+ from django.conf import settings
21
+ from django.contrib.staticfiles import finders
22
+ from django.contrib.staticfiles.storage import staticfiles_storage
23
+ from django.core.files.storage import FileSystemStorage, default_storage
24
+ from django.template.loader import render_to_string
25
+ from django.utils.module_loading import import_string
26
+
27
+ import weasyprint
28
+
29
+ from maykin_common.settings import get_setting
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ __all__ = ["render_to_pdf"]
34
+
35
+
36
+ def get_base_url(*args, **kwargs) -> str:
37
+ """
38
+ Get the base URL where the project is served.
39
+ """
40
+
41
+ if pdf_base_url_function := get_setting("PDF_BASE_URL_FUNCTION"):
42
+ return import_string(pdf_base_url_function)(*args, **kwargs)
43
+
44
+ raise NotImplementedError("You must implement 'get_base_url'.")
45
+
46
+
47
+ class UrlFetcher:
48
+ """
49
+ URL fetcher that skips the network for /static/* files.
50
+ """
51
+
52
+ def __init__(self):
53
+ self.static_url = self._get_fully_qualified_url(settings.STATIC_URL)
54
+ is_static_local_storage = issubclass(
55
+ staticfiles_storage.__class__, FileSystemStorage
56
+ )
57
+
58
+ self.media_url = self._get_fully_qualified_url(settings.MEDIA_URL)
59
+ is_media_local_storage = issubclass(
60
+ default_storage.__class__, FileSystemStorage
61
+ )
62
+
63
+ self.candidates = (
64
+ (self.static_url, staticfiles_storage, is_static_local_storage),
65
+ (self.media_url, default_storage, is_media_local_storage),
66
+ )
67
+
68
+ @staticmethod
69
+ def _get_fully_qualified_url(setting: str):
70
+ fully_qualified_url = setting
71
+ if not urlparse(setting).netloc:
72
+ fully_qualified_url = urljoin(get_base_url(), setting)
73
+ return urlparse(fully_qualified_url)
74
+
75
+ def __call__(self, url: str) -> dict:
76
+ orig_url = url
77
+ parsed_url = urlparse(url)
78
+
79
+ candidate = self.get_match_candidate(parsed_url)
80
+ if candidate is not None:
81
+ base_url, storage = candidate
82
+ path = PurePosixPath(parsed_url.path).relative_to(base_url.path)
83
+
84
+ absolute_path = None
85
+ if storage.exists(path):
86
+ absolute_path = storage.path(path)
87
+ elif settings.DEBUG and storage is staticfiles_storage:
88
+ # use finders so that it works in dev too, we already check that it's
89
+ # using filesystem storage earlier
90
+ absolute_path = finders.find(str(path))
91
+
92
+ if absolute_path is None:
93
+ logger.error("Could not resolve path '%s'", path)
94
+ return weasyprint.default_url_fetcher(orig_url)
95
+
96
+ content_type, encoding = mimetypes.guess_type(absolute_path)
97
+ result = dict(
98
+ mime_type=content_type,
99
+ encoding=encoding,
100
+ redirected_url=orig_url,
101
+ filename=path.parts[-1],
102
+ )
103
+ with open(absolute_path, "rb") as f:
104
+ result["file_obj"] = BytesIO(f.read())
105
+ return result
106
+ return weasyprint.default_url_fetcher(orig_url)
107
+
108
+
109
+ def render_to_pdf(template_name: str, context: dict) -> tuple[str, bytes]:
110
+ """
111
+ Render a (HTML) template to PDF with the given context.
112
+ """
113
+ rendered_html = render_to_string(template_name, context=context)
114
+ html_object = weasyprint.HTML(
115
+ string=rendered_html,
116
+ url_fetcher=UrlFetcher(),
117
+ base_url=get_base_url(),
118
+ )
119
+ pdf: bytes = html_object.write_pdf()
120
+ return rendered_html, pdf
File without changes
@@ -0,0 +1,34 @@
1
+ from django.conf import settings
2
+
3
+ GOOGLE_ANALYTICS_ID = None
4
+ """Google analytics id added to the request context"""
5
+
6
+
7
+ SHOW_ENVIRONMENT = False
8
+ """enable or disable the `environment_info` template tag."""
9
+
10
+ ENVIRONMENT_BACKGROUND_COLOR = "orange"
11
+ """The background color of the `environment_info` template tag."""
12
+
13
+ ENVIRONMENT_FOREGROUND_COLOR = "black"
14
+ """The foreground color of the `environment_info` template tag."""
15
+
16
+ ENVIRONMENT_LABEL = None
17
+ """The foreground color of the `environment_info` template tag."""
18
+
19
+ RELEASE = None
20
+ """The environment label shown in the `version_info` template tag."""
21
+
22
+ GIT_SHA = None
23
+ """The git hash shown in the `version_info` template tag."""
24
+
25
+ PDF_BASE_URL_FUNCTION = None
26
+ """
27
+ Function that returns the base url needed to download/resolve custom fonts and/or any
28
+ image URLs included in the document to render.
29
+ """
30
+
31
+
32
+ def get_setting(name: str):
33
+ default = globals()[name]
34
+ return getattr(settings, name, default)
@@ -0,0 +1,17 @@
1
+ .version {
2
+ color: var(--body-fg);
3
+ padding: 0 30px;
4
+ font-size: smaller;
5
+ display: flex;
6
+ flex-direction: column;
7
+ }
8
+
9
+ .env-info {
10
+ display: block;
11
+ align-self: center;
12
+ padding-inline: 8px;
13
+ padding-block: 5px;
14
+ background-color: var(--admin-env-info-background-color, var(--secondary));
15
+ color: var(--admin-env-info-color, var(--primary-fg));
16
+ border-radius: 4px;
17
+ }
@@ -0,0 +1,12 @@
1
+ {% load i18n static %}
2
+ <link rel="stylesheet" href="{% static 'css/maykin_common.css' %}">
3
+ <div class="version" title="Git SHA: {{ GIT_SHA|default:'' }}">
4
+ <span class="version__release">
5
+ {% blocktrans %}version {{ RELEASE }}{% endblocktrans %}
6
+ </span>
7
+ {% if GIT_SHA %}
8
+ <code class="version__git-hash">
9
+ {% blocktrans %}GIT SHA: {{ GIT_SHA }}{% endblocktrans %}
10
+ </code>
11
+ {% endif %}
12
+ </div>
@@ -0,0 +1,46 @@
1
+ from typing import Any
2
+
3
+ from django import template
4
+ from django.utils.html import escape, format_html, mark_safe
5
+
6
+ from maykin_common.settings import get_setting
7
+
8
+ register = template.Library()
9
+
10
+
11
+ @register.inclusion_tag("maykin_common/includes/version_info.html")
12
+ def show_version_info():
13
+ """
14
+ Template that displays version info.
15
+ """
16
+ return {
17
+ "RELEASE": get_setting("RELEASE"),
18
+ "GIT_SHA": get_setting("GIT_SHA"),
19
+ }
20
+
21
+
22
+ @register.simple_tag(takes_context=True)
23
+ def show_environment_info(context: dict[str, Any]) -> str:
24
+ """
25
+ Template that show the current ENVIRONMENT to an authenticated user.
26
+
27
+ Returns an empty string if SHOW_ENVIRONMENT is set to `False`
28
+ """
29
+ if not get_setting("SHOW_ENVIRONMENT"):
30
+ return ""
31
+ if (user := context.get("user")) is None or not user.is_authenticated:
32
+ return ""
33
+
34
+ style_tokens = {
35
+ "background-color": get_setting("ENVIRONMENT_BACKGROUND_COLOR"),
36
+ "color": get_setting("ENVIRONMENT_FOREGROUND_COLOR"),
37
+ }
38
+ _inline_style_bits = [
39
+ f"--admin-env-info-{key}: {value}".format(key=key, value=escape(value))
40
+ for key, value in style_tokens.items()
41
+ ]
42
+ return format_html(
43
+ """<div class="env-info" style="{style}">{label}</div>""",
44
+ label=get_setting("ENVIRONMENT_LABEL"),
45
+ style=mark_safe("; ".join(_inline_style_bits)),
46
+ )
@@ -0,0 +1,25 @@
1
+ from django import http
2
+ from django.template import TemplateDoesNotExist, loader
3
+ from django.views.decorators.csrf import requires_csrf_token
4
+ from django.views.defaults import ERROR_500_TEMPLATE_NAME
5
+
6
+
7
+ @requires_csrf_token
8
+ def server_error(
9
+ request, template_name=ERROR_500_TEMPLATE_NAME
10
+ ) -> http.HttpResponseServerError:
11
+ """
12
+ 500 error handler.
13
+ """
14
+
15
+ try:
16
+ template = loader.get_template(template_name)
17
+ except TemplateDoesNotExist:
18
+ if template_name != ERROR_500_TEMPLATE_NAME:
19
+ # Reraise if it's a missing custom template.
20
+ raise
21
+ return http.HttpResponseServerError(
22
+ b"<h1>Server Error (500)</h1>", content_type="text/html"
23
+ )
24
+ context = {"request": request}
25
+ return http.HttpResponseServerError(template.render(context))
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: maykin-common
3
+ Version: 0.1.0
4
+ Summary: Re-usable utilities for Maykin Django projects, centrally maintained instead of in default-project.
5
+ Author-email: Maykin Media <support@maykinmedia.nl>
6
+ License: Copyright 2025 Maykin Media
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13
+
14
+ Project-URL: Homepage, https://github.com/maykinmedia/django-common
15
+ Project-URL: Documentation, http://maykin-django-common.readthedocs.io/en/latest/
16
+ Project-URL: Bug Tracker, https://github.com/maykinmedia/django-common/issues
17
+ Project-URL: Source Code, https://github.com/maykinmedia/django-common
18
+ Keywords: django,maykin,boilerplate
19
+ Classifier: Development Status :: 3 - Alpha
20
+ Classifier: Framework :: Django
21
+ Classifier: Framework :: Django :: 4.2
22
+ Classifier: Framework :: Django :: 5.2
23
+ Classifier: Intended Audience :: Developers
24
+ Classifier: Operating System :: Unix
25
+ Classifier: Operating System :: MacOS
26
+ Classifier: Operating System :: Microsoft :: Windows
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Requires-Python: >=3.12
31
+ Description-Content-Type: text/x-rst
32
+ License-File: LICENSE
33
+ Requires-Dist: django>=4.2
34
+ Provides-Extra: tests
35
+ Requires-Dist: pytest; extra == "tests"
36
+ Requires-Dist: pytest-django; extra == "tests"
37
+ Requires-Dist: pytest-cov; extra == "tests"
38
+ Requires-Dist: tox; extra == "tests"
39
+ Requires-Dist: ruff; extra == "tests"
40
+ Provides-Extra: docs
41
+ Requires-Dist: sphinx; extra == "docs"
42
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
43
+ Provides-Extra: release
44
+ Requires-Dist: bump-my-version; extra == "release"
45
+ Provides-Extra: pdf
46
+ Requires-Dist: weasyprint; extra == "pdf"
47
+ Provides-Extra: mfa
48
+ Requires-Dist: django-admin-index; extra == "mfa"
49
+ Requires-Dist: maykin-2fa; extra == "mfa"
50
+ Provides-Extra: axes
51
+ Requires-Dist: django-axes; extra == "axes"
52
+ Dynamic: license-file
53
+
54
+
55
+
56
+ Welcome to maykin-common's documentation!
57
+ =================================================
58
+
59
+ :Version: 0.1.0
60
+ :Source: https://github.com/maykinmedia/django-common
61
+ :Keywords: ``<keywords>``
62
+ :PythonVersion: 3.12
63
+
64
+ |build-status| |code-quality| |ruff| |coverage| |docs|
65
+
66
+ |python-versions| |django-versions| |pypi-version|
67
+
68
+
69
+ <One liner describing the project>
70
+
71
+ .. contents::
72
+
73
+ .. section-numbering::
74
+
75
+ Features
76
+ ========
77
+
78
+ * ...
79
+ * ...
80
+
81
+ Installation
82
+ ============
83
+
84
+ Requirements
85
+ ------------
86
+
87
+ * Python 3.12 or above
88
+ * Django 4.2 or newer
89
+
90
+
91
+ Install
92
+ -------
93
+
94
+ .. code-block:: bash
95
+
96
+ pip install maykin-common
97
+
98
+
99
+ Usage
100
+ =====
101
+
102
+ <document or refer to docs>
103
+
104
+ Local development
105
+ =================
106
+
107
+ To install and develop the library locally, use::
108
+
109
+ .. code-block:: bash
110
+
111
+ pip install -e .[tests,coverage,docs,release]
112
+
113
+ When running management commands via ``django-admin``, make sure to add the root
114
+ directory to the python path (or use ``python -m django <command>``):
115
+
116
+ .. code-block:: bash
117
+
118
+ export PYTHONPATH=. DJANGO_SETTINGS_MODULE=testapp.settings
119
+ django-admin check
120
+ # or other commands like:
121
+ # django-admin makemessages -l nl
122
+
123
+
124
+ .. |build-status| image:: https://github.com/maykinmedia/django-common/workflows/Run%20CI/badge.svg
125
+ :alt: Build status
126
+ :target: https://github.com/maykinmedia/django-common/actions?query=workflow%3A%22Run+CI%22
127
+
128
+ .. |code-quality| image:: https://github.com/maykinmedia/django-common/workflows/Code%20quality%20checks/badge.svg
129
+ :alt: Code quality checks
130
+ :target: https://github.com/maykinmedia/django-common/actions?query=workflow%3A%22Code+quality+checks%22
131
+
132
+ .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
133
+ :target: https://github.com/astral-sh/ruff
134
+ :alt: Ruff
135
+
136
+ .. |coverage| image:: https://codecov.io/gh/maykinmedia/django-common/branch/main/graph/badge.svg
137
+ :target: https://codecov.io/gh/maykinmedia/django-common
138
+ :alt: Coverage status
139
+
140
+ .. |docs| image:: https://readthedocs.org/projects/django-common/badge/?version=latest
141
+ :target: https://maykin-django-common.readthedocs.io/en/latest/?badge=latest
142
+ :alt: Documentation Status
143
+
144
+ .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-common.svg
145
+
146
+ .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-common.svg
147
+
148
+ .. |pypi-version| image:: https://img.shields.io/pypi/v/django-common.svg
149
+ :target: https://pypi.org/project/django-common/
@@ -0,0 +1,27 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.rst
4
+ pyproject.toml
5
+ maykin_common/__init__.py
6
+ maykin_common/apps.py
7
+ maykin_common/checks.py
8
+ maykin_common/context_processors.py
9
+ maykin_common/django_two_factor_auth.py
10
+ maykin_common/migration_operations.py
11
+ maykin_common/mixins.py
12
+ maykin_common/pdf.py
13
+ maykin_common/py.typed
14
+ maykin_common/settings.py
15
+ maykin_common/views.py
16
+ maykin_common.egg-info/PKG-INFO
17
+ maykin_common.egg-info/SOURCES.txt
18
+ maykin_common.egg-info/dependency_links.txt
19
+ maykin_common.egg-info/requires.txt
20
+ maykin_common.egg-info/top_level.txt
21
+ maykin_common/management/__init__.py
22
+ maykin_common/management/commands/__init__.py
23
+ maykin_common/management/commands/clear_cache.py
24
+ maykin_common/static/maykin_common/css/version.css
25
+ maykin_common/templates/maykin_common/includes/version_info.html
26
+ maykin_common/templatetags/__init__.py
27
+ maykin_common/templatetags/maykin_common.py
@@ -0,0 +1,25 @@
1
+ django>=4.2
2
+
3
+ [axes]
4
+ django-axes
5
+
6
+ [docs]
7
+ sphinx
8
+ sphinx-rtd-theme
9
+
10
+ [mfa]
11
+ django-admin-index
12
+ maykin-2fa
13
+
14
+ [pdf]
15
+ weasyprint
16
+
17
+ [release]
18
+ bump-my-version
19
+
20
+ [tests]
21
+ pytest
22
+ pytest-django
23
+ pytest-cov
24
+ tox
25
+ ruff
@@ -0,0 +1 @@
1
+ maykin_common
@@ -0,0 +1,137 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "maykin-common"
7
+ version = "0.1.0"
8
+ description = "Re-usable utilities for Maykin Django projects, centrally maintained instead of in default-project."
9
+ authors = [
10
+ {name = "Maykin Media", email = "support@maykinmedia.nl"}
11
+ ]
12
+ readme = "README.rst"
13
+ license = {file = "LICENSE"}
14
+ keywords = ["django", "maykin", "boilerplate"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Django",
18
+ "Framework :: Django :: 4.2",
19
+ "Framework :: Django :: 5.2",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: Unix",
22
+ "Operating System :: MacOS",
23
+ "Operating System :: Microsoft :: Windows",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ ]
28
+ requires-python = ">=3.12"
29
+ dependencies = [
30
+ "django>=4.2"
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/maykinmedia/django-common"
35
+ Documentation = "http://maykin-django-common.readthedocs.io/en/latest/"
36
+ "Bug Tracker" = "https://github.com/maykinmedia/django-common/issues"
37
+ "Source Code" = "https://github.com/maykinmedia/django-common"
38
+
39
+ [project.optional-dependencies]
40
+ tests = [
41
+ "pytest",
42
+ "pytest-django",
43
+ "pytest-cov",
44
+ "tox",
45
+ "ruff",
46
+ # "pyright",
47
+ # "django-stubs",
48
+ ]
49
+ docs = [
50
+ "sphinx",
51
+ "sphinx-rtd-theme",
52
+ ]
53
+ release = [
54
+ "bump-my-version",
55
+ ]
56
+ pdf = [
57
+ "weasyprint"
58
+ ]
59
+ mfa = [
60
+ "django-admin-index",
61
+ "maykin-2fa"
62
+ ]
63
+ axes = [
64
+ "django-axes"
65
+ ]
66
+
67
+ [tool.setuptools.packages.find]
68
+ include = ["maykin_common*"]
69
+ namespaces = true
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ DJANGO_SETTINGS_MODULE = "testapp.settings"
74
+
75
+ [tool.bumpversion]
76
+ current_version = "0.1.0"
77
+ files = [
78
+ {filename = "pyproject.toml"},
79
+ {filename = "README.rst"},
80
+ {filename = "docs/conf.py"},
81
+ ]
82
+
83
+ [tool.coverage.run]
84
+ branch = true
85
+ source = ["maykin_common"]
86
+ omit = [
87
+ # migrations run while django initializes the test db
88
+ "*/migrations/*",
89
+ ]
90
+
91
+ [tool.coverage.report]
92
+ exclude_also = [
93
+ "if (typing\\.)?TYPE_CHECKING:",
94
+ "@(typing\\.)?overload",
95
+ "class .*\\(.*Protocol.*\\):",
96
+ "@(abc\\.)?abstractmethod",
97
+ "raise NotImplementedError",
98
+ "\\.\\.\\.",
99
+ "\\bpass$",
100
+ ]
101
+
102
+ [tool.pyright]
103
+ include = [
104
+ "{{ project_name|lower }}",
105
+ "testapp",
106
+ "tests",
107
+ ]
108
+ ignore = []
109
+ pythonVersion = "3.12"
110
+ pythonPlatform = "Linux"
111
+
112
+ [tool.ruff.lint]
113
+ extend-select = [
114
+ "UP", # pyupgrade
115
+ "DJ", # django
116
+ "LOG", # logging
117
+ "G",
118
+ "I", # isort
119
+ "E", # pycodestyle
120
+ "F", # pyflakes
121
+ "PERF",# perflint
122
+ "B", # flake8-bugbear
123
+ ]
124
+
125
+ [tool.ruff.lint.isort]
126
+ combine-as-imports = true
127
+ section-order = [
128
+ "future",
129
+ "standard-library",
130
+ "django",
131
+ "third-party",
132
+ "first-party",
133
+ "local-folder",
134
+ ]
135
+
136
+ [tool.ruff.lint.isort.sections]
137
+ "django" = ["django"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+