"""
Library of shortcuts.
"""
# Imports
from django.apps import apps as django_apps
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ImproperlyConfigured
from django.db.models import AutoField
from django.template import Context, loader, Template
from django.template.exceptions import TemplateDoesNotExist
from django.utils.module_loading import module_has_submodule
from importlib import import_module
from itertools import chain
from operator import attrgetter
from superdjango.conf import SUPERDJANGO
import warnings
# Exports
__all__ = (
"copy_model_instance",
"get_app_modules",
"get_model",
"get_setting",
"get_setting_with_prefix",
"get_user_name",
"has_o2o",
"parse_string",
"parse_template",
"static_file_exists",
"template_exists",
"there_can_be_only_one",
"title",
"user_in_group",
# "CombinedQuerySet",
"CompareQuerySet",
)
# Constants
DEBUG = getattr(settings, "DEBUG", False)
# Functions
[docs]def copy_model_instance(instance):
"""Copy an instance of a model.
:param instance: The model instance to be copied.
:type instance: Model
:returns: Returns a copy of the model instance.
.. tip::
The new instance has *not* been saved to the database. Also, reference fields must be manually recreated using
the original instance.
"""
initial_values = dict()
# noinspection PyProtectedMember
for f in instance._meta.fields:
# noinspection PyProtectedMember
if not isinstance(f, AutoField) and f not in list(instance._meta.parents.values()):
initial_values[f.name] = getattr(instance, f.name)
return instance.__class__(**initial_values)
[docs]def get_app_modules(name):
"""Yields tuples of ``(app_name, module)`` for each installed app that includes the given module name.
:param name: The module name.
:type name: str
"""
for app in django_apps.get_app_configs():
if module_has_submodule(app.module, name):
yield app.name, import_module("%s.%s" % (app.name, name))
[docs]def get_model(dotted_path, raise_exception=True):
"""Get the model for the given dotted path.
:param dotted_path: The ``app_label.ModelName`` path of the model to return.
:type dotted_path: str
:param raise_exception: Raise an exception on failure.
:type raise_exception: bool
"""
try:
return django_apps.get_model(dotted_path, require_ready=False)
except ValueError:
if raise_exception:
raise ImproperlyConfigured('dotted_path must be in the form of "app_label.ModelName".')
except LookupError:
if raise_exception:
raise ImproperlyConfigured("dotted_path refers to a model that has not been installed: %s" % dotted_path)
return None
[docs]def get_setting(name, default=None):
"""Get the value of the named setting from the ``settings.py`` file.
:param name: The name of the setting.
:type name: str
:param default: The default value.
"""
message = "get_settings() will likely be removed in the next minor version. Use " \
"conf.SUPERDJANGO.get() instead."
warnings.warn(message, PendingDeprecationWarning)
return getattr(settings, name.upper(), default)
[docs]def get_setting_with_prefix(name, default=None, prefix="SUPERDJANGO"):
"""Get the value of the named setting from the ``settings.py`` file.
:param name: The name of the setting.
:type name: str
:param default: The default value.
:param prefix: The setting prefix.
:type prefix: str
"""
message = "get_setting_with_prefix() will likely be removed in the next minor version. Use " \
"conf.SUPERDJANGO.get() instead."
warnings.warn(message, PendingDeprecationWarning)
full_name = "%s_%s" % (prefix, name.upper())
return get_setting(full_name, default=default)
[docs]def get_user_name(user):
"""Get the full name of the user, if available, or just the value of the username field if not.
:param user: The user instance.
:rtype: str | None
:raises: ``AttributeError`` if a custom user model has been implemented without a ``get_full_name()`` method.
.. tip::
It is safe to call this function at runtime where user is ``None``.
"""
if user is None:
return None
full_name = user.get_full_name()
if len(full_name) > 0:
return full_name
return getattr(user, user.USERNAME_FIELD)
[docs]def has_o2o(instance, field_name):
"""Test whether a OneToOneField is populated.
:param instance: The model instance to check.
:type instance: object
:param field_name: The name of the one to one field.
:type field_name: str || unicode
:rtype: bool
This is the same `hasattr()``, but is more explicit and easier to remember.
See http://devdocs.io/django/topics/db/examples/one_to_one
"""
return hasattr(instance, field_name)
[docs]def parse_string(string, context):
"""Parse a string as a Django template.
:param string: The name of the template.
:type string: str
:param context: Context variables.
:type context: dict
:rtype: str
"""
message = "parse_string() has been moved to superdjango.html.shortcuts and will be removed in the next " \
"minor version."
warnings.warn(message, PendingDeprecationWarning)
template = Template(string)
return template.render(Context(context))
[docs]def parse_template(template, context):
"""Ad hoc means of parsing a template using Django's built-in loader.
:param template: The name of the template.
:type template: str || unicode
:param context: Context variables.
:type context: dict
:rtype: str
"""
message = "parse_template() has been moved to superdjango.html.shortcuts and will be removed in the next " \
"minor version."
warnings.warn(message, PendingDeprecationWarning)
return loader.render_to_string(template, context)
[docs]def static_file_exists(path, tokens=None):
"""Determine whether a static file exists.
:param path: The path to be checked.
:type path: str
:param tokens: If given, format will be used to replace the tokens in the path.
:type tokens: dict
:rtype: bool
"""
if tokens is not None:
path = path.format(**tokens)
return finders.find(path) is not None
[docs]def template_exists(name):
"""Indicates whether the given template exists.
:param name: The name (path) of the template to load.
:type name: str
:rtype: bool
"""
message = "template_exists() has been moved to superdjango.html.shortcuts and will be removed in the next " \
"minor version."
warnings.warn(message, PendingDeprecationWarning)
try:
loader.get_template(name)
return True
except TemplateDoesNotExist:
return False
[docs]def there_can_be_only_one(cls, instance, field_name):
"""Helper function that ensures a given boolean field is ``True`` for only one record in the database. It is
intended for use with ``pre_save`` signals.
:param cls: The class (sender) emitting the signal.
:type cls: class
:param instance: The instance to be checked.
:type: instance: object
:param field_name: The name of the field to be checked. Must be a ``BooleanField``.
:type field_name: str | unicode
:raises: ``AttributeError`` if the ``field_name`` does not exist on the ``instance``.
"""
field_value = getattr(instance, field_name)
if field_value:
cls.objects.update(**{field_name: False})
[docs]def title(value, uppers=None):
"""Smart title conversion.
:param value: The value to converted to Title Case.
:type value: str
:param uppers: A list of keywords that are alway upper case.
:type uppers: list[str]
:rtype: str
"""
if uppers is None:
uppers = list()
uppers += SUPERDJANGO.UPPERS
tokens = value.split(" ")
_value = list()
for t in tokens:
if t.lower() in uppers:
v = t.upper()
else:
v = t.title()
_value.append(v)
return " ".join(_value)
[docs]def user_in_group(user, group):
"""Determine whether a given user is in a particular group.
:param user: The user instance to be checked.
:type user: User
:param group: The name of the group.
:type group: str
:rtype: bool
"""
return user.groups.filter(name=group).exists()
# Classes
# TODO: Original ideas for CombinedQuerySet doesn't really work. Needs lots of thought and testing to mimic a queryset.
# Will work on this again when I have a need for a combined qs.
# class CombinedQuerySet(object):
# """Combines two are more querysets as though they were one."""
#
# def __init__(self, querysets=None):
# if querysets is not None:
# self.rows = querysets
# else:
# self.rows = list()
#
# if querysets is not None:
# self.results = list(chain(*querysets))
# else:
# self.results = list()
#
# def append(self, qs):
# self.results.append(chain(qs))
#
# def filter(self, **criteria):
# _results = list()
# for qs in self.results:
# new_qs = qs.filter(**criteria)
# _results.append(new_qs)
#
# return CombinedQuerySet(querysets=_results)
#
# def order_by(self, field_name):
# return sorted(chain(*self.results), key=attrgetter(field_name))
[docs]class CompareQuerySet(object):
"""Compare two querysets to see what's been added or removed."""
[docs] def __init__(self, qs1, qs2):
"""Initialize the comparison.
:param qs1: The primary or "authoritative" queryset.
:type qs1: django.db.models.QuerySet
:param qs2: The secondary queryset.
:type qs2: django.db.models.QuerySet
"""
self.qs1 = qs1
self.qs2 = qs2
self._pk1 = list()
for row in qs1:
self._pk1.append(row.pk)
self._pk2 = list()
for row in qs2:
self._pk2.append(row.pk)
[docs] def get_added(self):
"""Get the primary keys of records in qs2 that do not appear in qs1.
:rtype: list
"""
# Faster method for larger querysets?
# https://stackoverflow.com/a/3462160/241720
a = list()
for pk in self._pk2:
if pk not in self._pk1:
a.append(pk)
return a
[docs] def get_removed(self):
"""Get the primary keys of records in qs2 that do not appear in qs1.
:rtype: list
"""
# Faster method for larger querysets?
# https://stackoverflow.com/a/3462160/241720
a = list()
for pk in self._pk2:
if pk not in self._pk1:
a.append(pk)
return a
[docs] def is_different(self):
"""Indicates whether the two querysets are different.
:rtype: bool
"""
return set(self._pk1) != set(self._pk2)
[docs] def is_same(self):
"""Indicates whether the two querysets are the same.
:rtype: bool
"""
return set(self._pk1) == set(self._pk2)