├── solo
├── py.typed
├── tests
│ ├── __init__.py
│ ├── testapp2
│ │ ├── __init__.py
│ │ └── models.py
│ ├── models.py
│ ├── settings.py
│ └── tests.py
├── templatetags
│ ├── __init__.py
│ └── solo_tags.py
├── locale
│ ├── de
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ └── es
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── apps.py
├── __init__.py
├── templates
│ └── admin
│ │ └── solo
│ │ ├── object_history.html
│ │ └── change_form.html
├── settings.py
├── models.py
└── admin.py
├── examples
└── config
│ ├── __init__.py
│ ├── admin.py
│ └── models.py
├── .gitattributes
├── docs
└── images
│ └── django-solo-admin.jpg
├── .gitignore
├── MANIFEST.in
├── LICENSE
├── .github
└── workflows
│ └── python.yml
├── manage.py
├── tox.ini
├── pyproject.toml
├── CHANGES
└── README.md
/solo/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/solo/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/solo/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/solo/tests/testapp2/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/docs/images/django-solo-admin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lazybird/django-solo/HEAD/docs/images/django-solo-admin.jpg
--------------------------------------------------------------------------------
/solo/locale/de/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lazybird/django-solo/HEAD/solo/locale/de/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/solo/locale/es/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lazybird/django-solo/HEAD/solo/locale/es/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | .tox
3 | .idea
4 | __pycache__
5 | *.pyc
6 | venv
7 | dist
8 | files
9 | .mypy_cache
10 |
11 | .DS_STORE
12 |
--------------------------------------------------------------------------------
/solo/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SoloAppConfig(AppConfig):
5 | name = "solo"
6 | verbose_name = "solo"
7 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md
2 | include LICENSE
3 | include CHANGES
4 | include solo/py.typed
5 | recursive-include solo/templates *
6 | recursive-include solo/locale *.mo *.po
7 |
--------------------------------------------------------------------------------
/solo/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | django-solo helps working with singletons:
3 | things like global settings that you want to edit from the admin site.
4 | """
5 |
6 | __version__ = "2.4.0"
7 |
--------------------------------------------------------------------------------
/solo/tests/testapp2/models.py:
--------------------------------------------------------------------------------
1 | from solo.models import SingletonModel
2 |
3 |
4 | class SiteConfiguration(SingletonModel):
5 | class Meta:
6 | verbose_name = "Site Configuration 2"
7 |
--------------------------------------------------------------------------------
/examples/config/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from config.models import SiteConfiguration
4 | from solo.admin import SingletonModelAdmin
5 |
6 | admin.site.register(SiteConfiguration, SingletonModelAdmin)
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Licence and Credits
2 | ===================
3 |
4 | This work was originally founded by [Trapeze][trapeze] and is licensed under a
5 | [Creative Commons Attribution 3.0 Unported][licence].
6 |
7 |
8 | [licence]: http://creativecommons.org/licenses/by/3.0/
9 | [trapeze]: http://trapeze.com/
10 |
--------------------------------------------------------------------------------
/examples/config/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from solo.models import SingletonModel
4 |
5 |
6 | class SiteConfiguration(SingletonModel):
7 | site_name = models.CharField(max_length=255, default="Site Name")
8 | maintenance_mode = models.BooleanField(default=False)
9 |
10 | def __str__(self):
11 | return "Site Configuration"
12 |
13 | class Meta:
14 | verbose_name = "Site Configuration"
15 | verbose_name_plural = "Site Configuration"
16 |
--------------------------------------------------------------------------------
/solo/templates/admin/solo/object_history.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/object_history.html" %}
2 | {% load i18n %}
3 |
4 | {% block breadcrumbs %}
5 | {% if skip_object_list_page %}
6 |
12 | {% else %}
13 | {{ block.super }}
14 | {% endif %}
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/solo/settings.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.conf import settings
4 |
5 | # template parameters
6 | GET_SOLO_TEMPLATE_TAG_NAME: str = getattr(settings, "GET_SOLO_TEMPLATE_TAG_NAME", "get_solo")
7 |
8 | SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE: bool = getattr(settings, "SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE", True)
9 |
10 | # The cache that should be used, e.g. 'default'. Refers to Django CACHES setting.
11 | # Set to None to disable caching.
12 | SOLO_CACHE: str | None = None
13 |
14 | SOLO_CACHE_TIMEOUT = 60 * 5
15 |
16 | SOLO_CACHE_PREFIX = "solo"
17 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
18 |
--------------------------------------------------------------------------------
/.github/workflows/python.yml:
--------------------------------------------------------------------------------
1 | name: python
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tox:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v5
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | python -m pip install tox tox-gh-actions
21 | - name: Test with tox
22 | run: tox
23 |
--------------------------------------------------------------------------------
/solo/templates/admin/solo/change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 | {% load i18n %}
3 | {% load admin_urls %}
4 |
5 | {% block breadcrumbs %}
6 | {% if skip_object_list_page %}
7 |
12 | {% else %}
13 | {{ block.super }}
14 | {% endif %}
15 | {% endblock %}
16 |
17 | {% block object-tools-items %}
18 | {% trans "History" %}
19 | {% if has_absolute_url %}
20 | {% trans "View on site" %}
21 | {% endif %}
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks.
3 |
4 | Note: For django-solo, this file is used simply to launch the test suite.
5 | """
6 |
7 | import os
8 | import sys
9 |
10 |
11 | def main():
12 | """Run administrative tasks."""
13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
14 | try:
15 | from django.core.management import execute_from_command_line
16 | except ImportError as exc:
17 | raise ImportError(
18 | "Couldn't import Django. Are you sure it's installed and "
19 | "available on your PYTHONPATH environment variable? Did you "
20 | "forget to activate a virtual environment?"
21 | ) from exc
22 | execute_from_command_line(sys.argv)
23 |
24 |
25 | if __name__ == "__main__":
26 | main()
27 |
--------------------------------------------------------------------------------
/solo/tests/models.py:
--------------------------------------------------------------------------------
1 | from django.core.files.uploadedfile import SimpleUploadedFile
2 | from django.db import models
3 |
4 | from solo.models import SingletonModel
5 |
6 |
7 | class SiteConfiguration(SingletonModel):
8 | site_name = models.CharField(max_length=255, default="Default Config")
9 | file = models.FileField(upload_to="files", default=SimpleUploadedFile("default-file.pdf", None))
10 |
11 | def __str__(self):
12 | return "Site Configuration"
13 |
14 | class Meta:
15 | verbose_name = "Site Configuration"
16 |
17 |
18 | class SiteConfigurationWithExplicitlyGivenId(SingletonModel):
19 | singleton_instance_id = 24
20 | site_name = models.CharField(max_length=255, default="Default Config")
21 |
22 | def __str__(self):
23 | return "Site Configuration"
24 |
25 | class Meta:
26 | verbose_name = "Site Configuration"
27 |
--------------------------------------------------------------------------------
/solo/tests/settings.py:
--------------------------------------------------------------------------------
1 | MIDDLEWARE_CLASSES = (
2 | "django.contrib.sessions.middleware.SessionMiddleware",
3 | "django.middleware.locale.LocaleMiddleware",
4 | "django.middleware.common.CommonMiddleware",
5 | "django.middleware.csrf.CsrfViewMiddleware",
6 | "django.contrib.auth.middleware.AuthenticationMiddleware",
7 | "django.contrib.messages.middleware.MessageMiddleware",
8 | )
9 |
10 | DATABASES = {
11 | "default": {
12 | "ENGINE": "django.db.backends.sqlite3",
13 | "NAME": "solo-tests.db",
14 | }
15 | }
16 |
17 | INSTALLED_APPS = (
18 | "solo",
19 | "solo.tests",
20 | "solo.tests.testapp2",
21 | )
22 |
23 | SECRET_KEY = "any-key"
24 |
25 | CACHES = {
26 | "default": {
27 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
28 | "LOCATION": "127.0.0.1:11211",
29 | },
30 | }
31 |
32 | SOLO_CACHE = "default"
33 |
34 | TEMPLATES = [
35 | {
36 | "BACKEND": "django.template.backends.django.DjangoTemplates",
37 | "APP_DIRS": True,
38 | },
39 | ]
40 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
41 |
--------------------------------------------------------------------------------
/solo/templatetags/solo_tags.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.apps import apps
3 | from django.utils.translation import gettext as _
4 |
5 | from solo import settings as solo_settings
6 | from solo.models import SingletonModel
7 |
8 | register = template.Library()
9 |
10 |
11 | @register.simple_tag(name=solo_settings.GET_SOLO_TEMPLATE_TAG_NAME)
12 | def get_solo(model_path: str) -> SingletonModel:
13 | try:
14 | app_label, model_name = model_path.rsplit(".", 1)
15 | except ValueError:
16 | raise template.TemplateSyntaxError(
17 | _(
18 | "Templatetag requires the model dotted path: 'app_label.ModelName'. "
19 | "Received '{model_path}'."
20 | ).format(model_path=model_path)
21 | )
22 | try:
23 | model_class: type[SingletonModel] = apps.get_model(app_label, model_name)
24 | except LookupError:
25 | raise template.TemplateSyntaxError(
26 | _("Could not get the model name '{model}' from the application named '{app}'").format(
27 | model=model_name, app=app_label
28 | )
29 | )
30 | return model_class.get_solo()
31 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Configure which test environments are run for each Github Actions Python version.
2 | [gh-actions]
3 | python =
4 | 3.9: py39-django{42}
5 | 3.10: py310-django{42,50}, type-check, lint
6 | 3.11: py311-django{42,50,51,52}
7 | 3.12: py312-django{42,50,51,52}
8 | 3.13: py313-django{42,50,51,52}
9 |
10 | [tox]
11 | envlist =
12 | type-check
13 | lint
14 | py{38,39,310}-django{42,50}
15 | py{311,312,313}-django{42,50,51,52}
16 |
17 | [testenv]
18 | deps =
19 | django42: Django>=4.2,<4.3
20 | django50: Django>=5.0,<5.1
21 | django51: Django>=5.1,<5.2
22 | django52: Django>=5.2,<5.3
23 | commands =
24 | {envpython} {toxinidir}/manage.py test solo --settings=solo.tests.settings
25 |
26 | [testenv:build]
27 | skip_install = true
28 | deps =
29 | build
30 | commands =
31 | {envpython} -m build
32 |
33 | [testenv:upload]
34 | skip_install = true
35 | deps =
36 | twine
37 | commands =
38 | {envpython} -m twine upload {toxinidir}/dist/*
39 |
40 | [testenv:type-check]
41 | skip_install = true
42 | deps =
43 | mypy==1.12.0
44 | django-stubs==5.1.0
45 | commands =
46 | mypy solo
47 |
48 | [testenv:lint]
49 | skip_install = true
50 | deps =
51 | ruff==0.7.0
52 | commands =
53 | ruff format --check
54 | ruff check
55 |
--------------------------------------------------------------------------------
/solo/locale/de/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # German translation for django-solo.
2 | # Copyright (C) 2022
3 | # This file is distributed under the same license as the django-solo package.
4 | # Conrad , 2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2020-04-13 16:13+0200\n"
11 | "PO-Revision-Date: 2008-11-30 12:12\n"
12 | "Last-Translator: \n"
13 | "Language-Team: de\n"
14 | "Language: de\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "X-Translated-Using: django-rosetta 0.9.3\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 |
21 | #: solo/admin.py:53
22 | #, python-format
23 | msgid "%(obj)s was changed successfully."
24 | msgstr "%(obj)s wurde erfolgreich geändert."
25 |
26 | #: solo/admin.py:55
27 | msgid "You may edit it again below."
28 | msgstr "Sie können dies erneut anpassen."
29 |
30 | #: solo/templates/admin/solo/change_form.html:7
31 | #: solo/templates/admin/solo/object_history.html:6
32 | msgid "Home"
33 | msgstr "Home"
34 |
35 | #: solo/templates/admin/solo/change_form.html:14
36 | #: solo/templates/admin/solo/object_history.html:9
37 | msgid "History"
38 | msgstr "Verlauf"
39 |
40 | #: solo/templates/admin/solo/change_form.html:15
41 | msgid "View on site"
42 | msgstr "Auf der Seite anzeigen"
43 |
44 | #: solo/templatetags/solo_tags.py:22
45 | #, python-format
46 | msgid ""
47 | "Templatetag requires the model dotted path: 'app_label.ModelName'. Received "
48 | "'%s'."
49 | msgstr "Templatetag kann '%s' nicht verarbeiten. Model wird in der 'dotted path'-Schreibweise benötigt (Beispiel: 'app_label.ModelName')."
50 |
51 | #: solo/templatetags/solo_tags.py:28
52 | #, python-format
53 | msgid ""
54 | "Could not get the model name '%(model)s' from the application named '%(app)s'"
55 | msgstr ""
56 | "Model '%(model)s' konnte nicht in Applikation '%(app)s' gefunden werden."
57 |
--------------------------------------------------------------------------------
/solo/locale/es/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # Spanish translation for django-solo.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the django-solo package.
4 | # Álvaro Mondéjar , 2020.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2020-04-13 16:13+0200\n"
11 | "PO-Revision-Date: 2008-11-30 12:12\n"
12 | "Last-Translator: \n"
13 | "Language-Team: es\n"
14 | "Language: es\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "X-Translated-Using: django-rosetta 0.9.3\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 |
21 | #: solo/admin.py:53
22 | #, python-format
23 | msgid "%(obj)s was changed successfully."
24 | msgstr "%(obj)s fue editado correctamente."
25 |
26 | #: solo/admin.py:55
27 | msgid "You may edit it again below."
28 | msgstr "Puede volverlo a editar otra vez a continuación."
29 |
30 | #: solo/templates/admin/solo/change_form.html:7
31 | #: solo/templates/admin/solo/object_history.html:6
32 | msgid "Home"
33 | msgstr "Inicio"
34 |
35 | #: solo/templates/admin/solo/change_form.html:14
36 | #: solo/templates/admin/solo/object_history.html:9
37 | msgid "History"
38 | msgstr "Historial"
39 |
40 | #: solo/templates/admin/solo/change_form.html:15
41 | msgid "View on site"
42 | msgstr "Ver en el sitio"
43 |
44 | #: solo/templatetags/solo_tags.py:22
45 | #, python-format
46 | msgid ""
47 | "Templatetag requires the model dotted path: 'app_label.ModelName'. Received "
48 | "'%s'."
49 | msgstr "Templatetag requiere el path del modelo punteado. Recibido '%s'."
50 |
51 | #: solo/templatetags/solo_tags.py:28
52 | #, python-format
53 | msgid ""
54 | "Could not get the model name '%(model)s' from the application named '%(app)s'"
55 | msgstr ""
56 | "No se pudo obtener el nombre del modelo '%(model)s' de la aplicación llamada "
57 | "'%(app)s'"
58 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "django-solo"
7 | description = "Django Solo helps working with singletons"
8 | authors = [{name = "lazybird"}]
9 | maintainers = [
10 | {name = "John Hagen", email = "johnthagen@gmail.com"}
11 | ]
12 | readme = "README.md"
13 | requires-python = ">=3.9"
14 | classifiers = [
15 | "Framework :: Django :: 4.2",
16 | "Framework :: Django :: 5.0",
17 | "Framework :: Django :: 5.1",
18 | "Framework :: Django :: 5.2",
19 | "Intended Audience :: Developers",
20 | "Operating System :: OS Independent",
21 | "Programming Language :: Python",
22 | "Programming Language :: Python :: 3",
23 | "Programming Language :: Python :: 3.9",
24 | "Programming Language :: Python :: 3.10",
25 | "Programming Language :: Python :: 3.11",
26 | "Programming Language :: Python :: 3.12",
27 | "Programming Language :: Python :: 3.13",
28 | ]
29 | dependencies = [
30 | "django>=4.2",
31 | "typing-extensions>=4.0.1; python_version < '3.11'",
32 | ]
33 | license = {text = "Creative Commons Attribution 3.0 Unported"}
34 | dynamic = ["version"]
35 |
36 | [tool.setuptools.dynamic]
37 | version = {attr = "solo.__version__"}
38 |
39 | [project.urls]
40 | Homepage = "https://github.com/lazybird/django-solo/"
41 | Source = "https://github.com/lazybird/django-solo/"
42 | Changelog = "https://github.com/lazybird/django-solo/blob/master/CHANGES"
43 |
44 | [tool.mypy]
45 | ignore_missing_imports = true
46 | strict = true
47 | exclude = "solo/tests"
48 |
49 | [tool.ruff]
50 | line-length = 100
51 | target-version = "py39"
52 |
53 | [tool.ruff.lint]
54 | select = [
55 | "F", # pyflakes
56 | "E", # pycodestyle
57 | "I", # isort
58 | "N", # pep8-naming
59 | "UP", # pyupgrade
60 | "RUF", # ruff
61 | "B", # flake8-bugbear
62 | "C4", # flake8-comprehensions
63 | "PTH", # flake8-use-pathlib
64 | "SIM", # flake8-simplify
65 | "TID", # flake8-tidy-imports
66 | ]
67 |
68 | ignore = [
69 | "B904",
70 | ]
71 |
--------------------------------------------------------------------------------
/solo/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | import warnings
5 | from typing import Any
6 |
7 | from django.conf import settings
8 | from django.core.cache import BaseCache, caches
9 | from django.db import models
10 |
11 | from solo import settings as solo_settings
12 |
13 | if sys.version_info >= (3, 11):
14 | from typing import Self
15 | else:
16 | from typing_extensions import Self
17 |
18 |
19 | DEFAULT_SINGLETON_INSTANCE_ID = 1
20 |
21 |
22 | def get_cache(cache_name: str) -> BaseCache:
23 | warnings.warn(
24 | "'get_cache' is deprecated and will be removed in django-solo 2.4.0. "
25 | "Instead, use 'caches' from 'django.core.cache'.",
26 | DeprecationWarning,
27 | stacklevel=2,
28 | )
29 | return caches[cache_name]
30 |
31 |
32 | class SingletonModel(models.Model):
33 | singleton_instance_id = DEFAULT_SINGLETON_INSTANCE_ID
34 |
35 | class Meta:
36 | abstract = True
37 |
38 | def save(self, *args: Any, **kwargs: Any) -> None:
39 | self.pk = self.singleton_instance_id
40 | super().save(*args, **kwargs)
41 | self.set_to_cache()
42 |
43 | def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]:
44 | self.clear_cache()
45 | return super().delete(*args, **kwargs)
46 |
47 | @classmethod
48 | def clear_cache(cls) -> None:
49 | cache_name = getattr(settings, "SOLO_CACHE", solo_settings.SOLO_CACHE)
50 | if cache_name:
51 | cache = caches[cache_name]
52 | cache_key = cls.get_cache_key()
53 | cache.delete(cache_key)
54 |
55 | def set_to_cache(self) -> None:
56 | cache_name = getattr(settings, "SOLO_CACHE", solo_settings.SOLO_CACHE)
57 | if not cache_name:
58 | return None
59 | cache = caches[cache_name]
60 | cache_key = self.get_cache_key()
61 | timeout = getattr(settings, "SOLO_CACHE_TIMEOUT", solo_settings.SOLO_CACHE_TIMEOUT)
62 | cache.set(cache_key, self, timeout)
63 |
64 | @classmethod
65 | def get_cache_key(cls) -> str:
66 | prefix = getattr(settings, "SOLO_CACHE_PREFIX", solo_settings.SOLO_CACHE_PREFIX)
67 | # Include the model's module in the cache key so similarly named models from different
68 | # apps do not have the same cache key.
69 | return f"{prefix}:{cls.__module__.lower()}:{cls.__name__.lower()}"
70 |
71 | @classmethod
72 | def get_solo(cls) -> Self:
73 | cache_name = getattr(settings, "SOLO_CACHE", solo_settings.SOLO_CACHE)
74 | if not cache_name:
75 | obj, _ = cls.objects.get_or_create(pk=cls.singleton_instance_id)
76 | return obj
77 | cache = caches[cache_name]
78 | cache_key = cls.get_cache_key()
79 | obj = cache.get(cache_key)
80 | if not obj:
81 | obj, _ = cls.objects.get_or_create(pk=cls.singleton_instance_id)
82 | obj.set_to_cache()
83 | return obj
84 |
--------------------------------------------------------------------------------
/solo/admin.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from django.contrib import admin
6 | from django.db.models import Model
7 | from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
8 | from django.urls import URLPattern, re_path
9 | from django.utils.encoding import force_str
10 | from django.utils.translation import gettext as _
11 |
12 | from solo import settings as solo_settings
13 | from solo.models import DEFAULT_SINGLETON_INSTANCE_ID
14 |
15 |
16 | class SingletonModelAdmin(admin.ModelAdmin): # type: ignore[type-arg]
17 | object_history_template = "admin/solo/object_history.html"
18 | change_form_template = "admin/solo/change_form.html"
19 |
20 | def has_add_permission(self, request: HttpRequest) -> bool:
21 | return False
22 |
23 | def has_delete_permission(self, request: HttpRequest, obj: Model | None = None) -> bool:
24 | return False
25 |
26 | def get_urls(self) -> list[URLPattern]:
27 | urls = super().get_urls()
28 |
29 | if not solo_settings.SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE:
30 | return urls
31 |
32 | # _meta.model_name only exists on Django>=1.6 -
33 | # on earlier versions, use module_name.lower()
34 | try:
35 | model_name = self.model._meta.model_name
36 | except AttributeError:
37 | model_name = self.model._meta.module_name.lower()
38 |
39 | self.model._meta.verbose_name_plural = self.model._meta.verbose_name
40 | url_name_prefix = f"{self.model._meta.app_label}_{model_name}"
41 | custom_urls = [
42 | re_path(
43 | r"^history/$",
44 | self.admin_site.admin_view(self.history_view),
45 | {"object_id": str(self.singleton_instance_id)},
46 | name=f"{url_name_prefix}_history",
47 | ),
48 | re_path(
49 | r"^$",
50 | self.admin_site.admin_view(self.change_view),
51 | {"object_id": str(self.singleton_instance_id)},
52 | name=f"{url_name_prefix}_change",
53 | ),
54 | ]
55 |
56 | # By inserting the custom URLs first, we overwrite the standard URLs.
57 | return custom_urls + urls
58 |
59 | def response_change(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect:
60 | msg = _("{obj} was changed successfully.").format(obj=force_str(obj))
61 | if "_continue" in request.POST:
62 | self.message_user(request, msg + " " + _("You may edit it again below."))
63 | return HttpResponseRedirect(request.path)
64 | else:
65 | self.message_user(request, msg)
66 | return HttpResponseRedirect("../../")
67 |
68 | def change_view(
69 | self,
70 | request: HttpRequest,
71 | object_id: str,
72 | form_url: str = "",
73 | extra_context: dict[str, Any] | None = None,
74 | ) -> HttpResponse:
75 | if object_id == str(self.singleton_instance_id):
76 | self.model.objects.get_or_create(pk=self.singleton_instance_id)
77 |
78 | if not extra_context:
79 | extra_context = {}
80 | extra_context["skip_object_list_page"] = solo_settings.SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE
81 |
82 | return super().change_view(
83 | request,
84 | object_id,
85 | form_url=form_url,
86 | extra_context=extra_context,
87 | )
88 |
89 | def history_view(
90 | self, request: HttpRequest, object_id: str, extra_context: dict[str, Any] | None = None
91 | ) -> HttpResponse:
92 | if not extra_context:
93 | extra_context = {}
94 | extra_context["skip_object_list_page"] = solo_settings.SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE
95 |
96 | return super().history_view(
97 | request,
98 | object_id,
99 | extra_context=extra_context,
100 | )
101 |
102 | @property
103 | def singleton_instance_id(self) -> int:
104 | return getattr(self.model, "singleton_instance_id", DEFAULT_SINGLETON_INSTANCE_ID)
105 |
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | Unreleased
2 | ==========
3 |
4 | * Add support for Django 5.2
5 |
6 | django-solo-2.4.0
7 | =================
8 |
9 | Date: 19 October, 2024
10 |
11 | * Fix similarly named models from different apps having the same cache key
12 | * Drop support for Python 3.8
13 | * Add support for Python 3.13
14 | * Drop support for end of life Django 3.2
15 | * Add support for Django 5.1
16 | * Improve error handling in template tag
17 |
18 | django-solo-2.3.0
19 | =================
20 |
21 | Date: 1 July, 2024
22 |
23 | * Add typing support
24 | * Deprecate `solo.models.get_cache`
25 | * Switch to `pyproject.toml`
26 | * Switch to Ruff for formatting and linting
27 |
28 | django-solo-2.2.0
29 | =================
30 |
31 | Date: 1 January, 2024
32 |
33 | * Add support for Python 3.12
34 | * Drop support for Python 3.7
35 | * Add support for Django 5.0
36 | * Drop support for end of life Django 4.0 and 4.1
37 |
38 | django-solo-2.1.0
39 | =================
40 |
41 | Date: 25 May, 2023
42 |
43 | * Add support for Django 4.1 and 4.2
44 | * Drop support for end of life Django 2.2
45 | * Add support for Python 3.11
46 | * Drop support for end of life Python 3.6
47 |
48 | django-solo-2.0.0
49 | =================
50 |
51 | Date: 10 December, 2021
52 |
53 | * Add support for Django 4.0
54 | * Drop support for end of life Django versions (only 2.2, 3.2, and 4.0 are supported).
55 | * Add GitHub Actions CI and tox
56 |
57 | django-solo-1.2.0
58 | =================
59 |
60 | Date: 29 September, 2021
61 |
62 | * Make the necessary changes to make the library compatible with Django 3.2
63 |
64 |
65 | * * *
66 |
67 | django-solo-1.1.5
68 | =================
69 |
70 | Date: 19 December, 2020
71 |
72 | * Fix setup long description - for pypi page
73 |
74 |
75 | * * *
76 |
77 | django-solo-1.1.4
78 | =================
79 |
80 | Date: 19 December, 2020
81 |
82 | * Now using the same README file github and pypi
83 | * Add pypi badge in Readme
84 | * Add a flag to allow not skipping the admin list page
85 | * Set the zip_safe option to False
86 | * refactor clear_cache into a classmethod
87 | * Added Spanish locale.
88 | * Missing import os added
89 | * update change_form for newer versions of Django
90 |
91 |
92 | * * *
93 |
94 | django-solo-1.1.3
95 | =================
96 |
97 | Date: 15 January, 2018
98 |
99 |
100 | * Merge pull request #64 from the-vishal/patch-2
101 | * Merge pull request #59 from hefting/fix-template-context
102 | * Retrieve app_label from opts in template context - Since Django 1.7
103 | * Fixes #57 -- read SOLO_CACHE_PREFIX from main Django settings
104 | * Merge pull request #56 from girogiro/master
105 | * Fixed #54 -- wrong caching of FileField
106 | * Merge pull request #55 from jaimesanz/master
107 | * Fixed RemovedInDjango20Warning deprecation warning
108 | * Merge pull request #50 from m16a1/patch-1
109 | * .delete() actually deletes now (#48)
110 | * Fix #45 add form_url to change_view (#46)
111 | * Added support for configuring the pk value of the singleton instance (#42)
112 |
113 | * * *
114 |
115 | django-solo-1.1.2
116 | =================
117 |
118 | Date: February 7, 2016
119 |
120 | * Documentation update - Elaborated on usage of the template tag
121 | * #37: Update custom_urls in admin.py due to deprecation warning
122 | * #39: Deprecation warning in tests - `TEMPLATES` setting
123 |
124 |
125 | * * *
126 | django-solo-1.1.1
127 | =================
128 |
129 | Date: December 9, 2015
130 |
131 | * Add apps module with a basic app config class.
132 | * Change the use of get_cache for Django 1.9.
133 | * Fixed warning messages showing up on Django 1.8.
134 | * Remove 'load url from future' tag from template (1.9).
135 | * Change the way `get_model` is imported.
136 |
137 |
138 | * * *
139 |
140 |
141 | django-solo-1.1.0
142 | =================
143 |
144 | Date: November 2, 2014
145 |
146 | * Fixed warning messages showing up on Django 1.7.
147 | * No need to define a plural name anymore - Pull Request #16.
148 | * Fixed some inconsistent variable names.
149 | * Added 'load url from future' so templates work with Django 1.4 - Pull Request #14.
150 |
151 |
152 | * * *
153 |
154 |
155 | django-solo-1.0.5
156 | =================
157 |
158 | Date: April 27, 2014
159 |
160 |
161 | * Pull Request #8: Python3 compatibility issue with `force_unicode` import statement.
162 |
163 |
164 | * * *
165 |
166 |
167 | django-solo-1.0.4
168 | =================
169 |
170 | Date: November 6, 2013
171 |
172 |
173 | * Issue #4: Django 1.6 compatibility on `url` and `pattern` import path.
174 |
175 |
176 | * * *
177 |
178 |
179 | django-solo-1.0.3
180 | =================
181 |
182 | Date: September 14, 2013
183 |
184 | * Fixed some packaging issues (licence file).
185 |
186 |
187 | * * *
188 |
189 | django-solo-1.0.2
190 | =================
191 |
192 | Date: September 14, 2013
193 |
194 | * Fixed some packaging issues.
195 |
196 |
197 | * * *
198 |
199 |
200 | django-solo-1.0.1
201 | =================
202 |
203 | Date: September 14, 2013
204 |
205 | * Added unit tests
206 | * Added support for @override_settings
207 | * Updated doc, licence and packaging
208 |
209 |
210 | * * *
211 |
212 | django-solo-1.0.0
213 | =================
214 |
215 | Date: July 11, 2013
216 |
217 | * Fist release of django-solo.
218 |
219 |
220 | * * *
221 |
--------------------------------------------------------------------------------
/solo/tests/tests.py:
--------------------------------------------------------------------------------
1 | from django.core.cache import caches
2 | from django.core.files.uploadedfile import SimpleUploadedFile
3 | from django.template import Context, Template, TemplateSyntaxError
4 | from django.test import TestCase
5 | from django.test.utils import override_settings
6 |
7 | from solo.tests.models import SiteConfiguration, SiteConfigurationWithExplicitlyGivenId
8 | from solo.tests.testapp2.models import SiteConfiguration as SiteConfiguration2
9 |
10 |
11 | class SingletonTest(TestCase):
12 | def setUp(self):
13 | self.template = Template(
14 | "{% load solo_tags %}"
15 | '{% get_solo "tests.SiteConfiguration" as site_config %}'
16 | "{{ site_config.site_name }}"
17 | "{{ site_config.file.url }}"
18 | )
19 | self.template_invalid_app = Template(
20 | "{% load solo_tags %}"
21 | '{% get_solo "invalid_app.SiteConfiguration" as site_config %}'
22 | "{{ site_config.site_name }}"
23 | "{{ site_config.file.url }}"
24 | )
25 | self.template_invalid_model = Template(
26 | "{% load solo_tags %}"
27 | '{% get_solo "tests.InvalidModel" as site_config %}'
28 | "{{ site_config.site_name }}"
29 | "{{ site_config.file.url }}"
30 | )
31 | self.cache = caches["default"]
32 | self.cache_key = SiteConfiguration.get_cache_key()
33 | self.cache.clear()
34 | SiteConfiguration.objects.all().delete()
35 |
36 | def test_template_tag_renders_default_site_config(self):
37 | SiteConfiguration.objects.all().delete()
38 | # At this point, there is no configuration object and we expect a
39 | # one to be created automatically with the default name value as
40 | # defined in models.
41 | output = self.template.render(Context())
42 | self.assertIn("Default Config", output)
43 |
44 | def test_template_tag_renders_site_config(self):
45 | SiteConfiguration.objects.create(site_name="Test Config")
46 | output = self.template.render(Context())
47 | self.assertIn("Test Config", output)
48 |
49 | @override_settings(SOLO_CACHE="default")
50 | def test_template_tag_uses_cache_if_enabled(self):
51 | SiteConfiguration.objects.create(site_name="Config In Database")
52 | fake_configuration = {"site_name": "Config In Cache"}
53 | self.cache.set(self.cache_key, fake_configuration, 10)
54 | output = self.template.render(Context())
55 | self.assertNotIn("Config In Database", output)
56 | self.assertNotIn("Default Config", output)
57 | self.assertIn("Config In Cache", output)
58 |
59 | @override_settings(SOLO_CACHE=None)
60 | def test_template_tag_uses_database_if_cache_disabled(self):
61 | SiteConfiguration.objects.create(site_name="Config In Database")
62 | fake_configuration = {"site_name": "Config In Cache"}
63 | self.cache.set(self.cache_key, fake_configuration, 10)
64 | output = self.template.render(Context())
65 | self.assertNotIn("Config In Cache", output)
66 | self.assertNotIn("Default Config", output)
67 | self.assertIn("Config In Database", output)
68 |
69 | @override_settings(SOLO_CACHE="default")
70 | def test_delete_if_cache_enabled(self):
71 | self.assertEqual(SiteConfiguration.objects.count(), 0)
72 | self.assertIsNone(self.cache.get(self.cache_key))
73 |
74 | one_cfg = SiteConfiguration.get_solo()
75 | one_cfg.site_name = "TEST SITE PLEASE IGNORE"
76 | one_cfg.save()
77 | self.assertEqual(SiteConfiguration.objects.count(), 1)
78 | self.assertIsNotNone(self.cache.get(self.cache_key))
79 |
80 | one_cfg.delete()
81 | self.assertEqual(SiteConfiguration.objects.count(), 0)
82 | self.assertIsNone(self.cache.get(self.cache_key))
83 | self.assertEqual(SiteConfiguration.get_solo().site_name, "Default Config")
84 |
85 | @override_settings(SOLO_CACHE=None)
86 | def test_delete_if_cache_disabled(self):
87 | # As above, but without the cache checks
88 | self.assertEqual(SiteConfiguration.objects.count(), 0)
89 | one_cfg = SiteConfiguration.get_solo()
90 | one_cfg.site_name = "TEST (uncached) SITE PLEASE IGNORE"
91 | one_cfg.save()
92 | self.assertEqual(SiteConfiguration.objects.count(), 1)
93 | one_cfg.delete()
94 | self.assertEqual(SiteConfiguration.objects.count(), 0)
95 | self.assertEqual(SiteConfiguration.get_solo().site_name, "Default Config")
96 |
97 | @override_settings(SOLO_CACHE="default")
98 | def test_file_upload_if_cache_enabled(self):
99 | cfg = SiteConfiguration.objects.create(
100 | site_name="Test Config", file=SimpleUploadedFile("file.pdf", None)
101 | )
102 | output = self.template.render(Context())
103 | self.assertIn(cfg.file.url, output)
104 |
105 | @override_settings(SOLO_CACHE_PREFIX="other")
106 | def test_cache_prefix_overriding(self):
107 | key = SiteConfiguration.get_cache_key()
108 | prefix = key.partition(":")[0]
109 | self.assertEqual(prefix, "other")
110 |
111 | def test_template_tag_invalid_app_name(self):
112 | with self.assertRaises(TemplateSyntaxError):
113 | self.template_invalid_app.render(Context())
114 |
115 | def test_template_invalid_model_name(self):
116 | with self.assertRaises(TemplateSyntaxError):
117 | self.template_invalid_model.render(Context())
118 |
119 |
120 | class SingletonWithExplicitIdTest(TestCase):
121 | def setUp(self):
122 | SiteConfigurationWithExplicitlyGivenId.objects.all().delete()
123 |
124 | def test_when_singleton_instance_id_is_given_created_item_will_have_given_instance_id(self):
125 | item = SiteConfigurationWithExplicitlyGivenId.get_solo()
126 | self.assertEqual(item.pk, SiteConfigurationWithExplicitlyGivenId.singleton_instance_id)
127 |
128 |
129 | class SingletonsWithAmbiguousNameTest(TestCase):
130 | def test_cache_key_is_not_ambiguous(self):
131 | assert SiteConfiguration.get_cache_key() != SiteConfiguration2.get_cache_key()
132 |
133 | def test_get_solo_returns_the_correct_singleton(self):
134 | assert SiteConfiguration.get_solo() != SiteConfiguration2.get_solo()
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Django Solo
3 | ===========
4 |
5 |
6 |
7 |
8 | +---------------------------+
9 | | |
10 | | |
11 | | \ | Django Solo helps working with singletons:
12 | | /\ | database tables that only have one row.
13 | | >=)'> | Singletons are useful for things like global
14 | | \/ | settings that you want to edit from the admin
15 | | / | instead of having them in Django settings.py.
16 | | |
17 | | |
18 | +---------------------------+
19 |
20 |
21 | Features
22 | --------
23 |
24 | Solo helps you enforce instantiating only one instance of a model in django.
25 |
26 | * You define the model that will hold your singleton object.
27 | * django-solo gives helper parent class for your model and the admin classes.
28 | * You get an admin interface that's aware you only have one object.
29 | * You can retrieve the object from templates.
30 | * By enabling caching, the database is not queried intensively.
31 |
32 | Use Cases
33 | --------
34 |
35 | Django Solo is also great for use with singleton objects that have a one to many relationship. Like the use case below where you have a 'Home Slider" that has many "Slides".
36 |
37 | * Global or default settings
38 | * An image slider that has many slides
39 | * A page section that has sub-sections
40 | * A team bio with many team members
41 |
42 | There are many cases where it makes sense for the parent in a one to many relationship to be limited to a single instance.
43 |
44 | Usage Example
45 |
46 | ```python
47 | # models.py
48 |
49 | from django.db import models
50 | from solo.models import SingletonModel
51 |
52 |
53 | class SiteConfiguration(SingletonModel):
54 | site_name = models.CharField(max_length=255, default='Site Name')
55 | maintenance_mode = models.BooleanField(default=False)
56 |
57 | def __str__(self):
58 | return "Site Configuration"
59 |
60 | class Meta:
61 | verbose_name = "Site Configuration"
62 | ```
63 |
64 | ```python
65 | # admin.py
66 |
67 | from django.contrib import admin
68 | from solo.admin import SingletonModelAdmin
69 | from config.models import SiteConfiguration
70 |
71 |
72 | admin.site.register(SiteConfiguration, SingletonModelAdmin)
73 | ```
74 |
75 | ```python
76 | # There is only one item in the table, you can get it this way:
77 | from .models import SiteConfiguration
78 | config = SiteConfiguration.objects.get()
79 |
80 | # get_solo will create the item if it does not already exist
81 | config = SiteConfiguration.get_solo()
82 | ```
83 |
84 | In your model, note how you did not have to provide a `verbose_name_plural` field -
85 | That's because Django Solo uses the `verbose_name` instead.
86 |
87 | If you're changing an existing model (which already has some objects stored in the database) to a singleton model, you can explicitly provide the id of the row in the database for django-solo to use. This can be done by setting `singleton_instance_id` property on the model:
88 |
89 | ```python
90 | class SiteConfiguration(SingletonModel):
91 | singleton_instance_id = 24
92 | # (...)
93 | ```
94 |
95 | Installation
96 | ------------
97 |
98 | This application requires a supported version of Django.
99 |
100 | * Install the package using `pip install django-solo`
101 | * Add ``solo`` or ``solo.apps.SoloAppConfig`` to your ``INSTALLED_APPS`` setting.
102 |
103 | This is how you run tests:
104 |
105 | ./manage.py test solo --settings=solo.tests.settings
106 |
107 | And from within `tox`:
108 |
109 | ```
110 | python -m pip install tox
111 | tox
112 | ```
113 |
114 | Supported Languages
115 | -------------------
116 |
117 | - English
118 | - Spanish
119 | - German
120 |
121 | Admin
122 | -----
123 |
124 | The standard Django admin does not fit well when working with singleton,
125 | for instance, if you need some global site settings to be edited in the admin.
126 | Django Solo provides a modified admin for that.
127 |
128 |
129 | 
130 |
131 |
132 | * In the admin home page where all applications are listed, we have a `config`
133 | application that holds a singleton model for site configuration.
134 | * The configuration object can only be changed, there's no link for "add" (1).
135 | * The link to the configuration page (2) directly goes to the form page - no
136 | need for an intermediary object list page, since there's only one object.
137 | * The edit page has a modified breadcrumb (3) to avoid linking to the
138 | intermediary object list page.
139 | * From the edit page, we cannot delete the object (4) nor can we add a new one (5).
140 |
141 | If you wish to disable the skipping of the object list page, and have the default
142 | breadcrumbs, you should set `SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE` to `False` in your settings.
143 |
144 | Availability from templates
145 | ---------------------------
146 |
147 | The singleton object can be retrieved from template by giving the Django model
148 | dotted path:
149 |
150 | ```django
151 | {% get_solo 'app_label.ModelName' as my_config %}
152 | ```
153 |
154 | Example:
155 |
156 | ```django
157 | {% load solo_tags %}
158 | {% get_solo 'config.SiteConfiguration' as site_config %}
159 | {{ site_config.site_name }}
160 | {{ site_config.maintenance_mode }}
161 | ```
162 |
163 | If you're extending a template, be sure to use the tag in the proper scope.
164 |
165 | Right:
166 |
167 | ```django
168 | {% extends "index.html" %}
169 | {% load solo_tags %}
170 |
171 | {% block content %}
172 | {% get_solo 'config.SiteConfiguration' as site_config %}
173 | {{ site_config.site_name }}
174 | {% endblock content %}
175 | ```
176 |
177 | Wrong:
178 |
179 | ```django
180 | {% extends "index.html" %}
181 | {% load solo_tags %}
182 | {% get_solo 'config.SiteConfiguration' as site_config %}
183 |
184 | {% block content %}
185 | {{ site_config.site_name }}
186 | {% endblock content %}
187 | ```
188 |
189 |
190 | Caching
191 | -------
192 |
193 | By default caching is disabled: every time `get_solo` retrieves the singleton
194 | object, there will be a database query.
195 |
196 | You can enable caching to only query the database when initially retrieving the
197 | object. The cache will also be updated when updates are made from the admin.
198 |
199 | The cache timeout is controlled via the `SOLO_CACHE_TIMEOUT` settings.
200 | The cache backend to be used is controlled via the `SOLO_CACHE` settings.
201 |
202 |
203 | Settings
204 | --------
205 |
206 | ### Template tag name
207 |
208 | You can retrieve your singleton object in templates using the `get_solo`
209 | template tag.
210 |
211 | You can change the name `get_solo` using the
212 | `GET_SOLO_TEMPLATE_TAG_NAME` setting.
213 |
214 | ```python
215 | GET_SOLO_TEMPLATE_TAG_NAME = 'get_config'
216 | ```
217 |
218 | ### Admin override flag
219 |
220 | By default, the admin is overridden. But if you wish to keep the object list
221 | page (e.g. to customize actions), you can set the `SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE`
222 | to `False`.
223 |
224 | ```python
225 | SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE = True
226 | ```
227 |
228 | ### Cache backend
229 |
230 | Django provides a way to define multiple cache backends with the `CACHES`
231 | settings. If you want the singleton object to be cached separately, you
232 | could define the `CACHES` and the `SOLO_CACHE` settings like this:
233 |
234 | ```python
235 | CACHES = {
236 | 'default': {
237 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
238 | 'LOCATION': '127.0.0.1:11211',
239 | },
240 | 'local': {
241 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
242 | },
243 | }
244 |
245 | SOLO_CACHE = 'local'
246 | ```
247 |
248 | Caching will be disabled if set to `None`.
249 |
250 |
251 | ### Cache timeout
252 |
253 | The cache timeout in seconds.
254 |
255 | ```python
256 | SOLO_CACHE_TIMEOUT = 60*5 # 5 mins
257 | ```
258 |
259 | ### Cache prefix
260 |
261 | The prefix to use for the cache key.
262 |
263 | ```python
264 | SOLO_CACHE_PREFIX = 'solo'
265 | ```
266 |
267 | Getting the code
268 | ================
269 |
270 | The code is hosted at https://github.com/lazybird/django-solo/
271 |
272 | Check out the latest development version anonymously with:
273 |
274 | $ git clone git://github.com/lazybird/django-solo.git
275 |
276 | You can install the package in the "editable" mode like this:
277 |
278 | pip uninstall django-solo # just in case...
279 | pip install -e git+https://github.com/lazybird/django-solo.git#egg=django-solo
280 |
281 | You can also install a specific branch:
282 |
283 | pip install -e git+https://github.com/lazybird/django-solo.git@my-branch#egg=django-solo
284 |
285 | The package is now installed in your project and you can find the code.
286 |
287 | To run the unit tests:
288 |
289 | pip install tox
290 | tox
291 |
292 | ### Making a release
293 |
294 | 1. Update [`solo/__init__.py`](solo/__init__.py) `version`
295 |
296 | 2. Update [`CHANGES`](./CHANGES)
297 |
298 | 3. Make a new release on GitHub
299 |
300 | 4. Upload release to PyPI
301 |
302 | ```shell
303 | tox -e build
304 | ```
305 |
306 | ```shell
307 | tox -e upload
308 | ```
309 |
--------------------------------------------------------------------------------