├── 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 | ![django-solo admin](https://raw.github.com/lazybird/django-solo/master/docs/images/django-solo-admin.jpg "django-solo admin") 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 | --------------------------------------------------------------------------------