Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Library of shortcuts.
4"""
6# Imports
8from django.apps import apps as django_apps
9from django.conf import settings
10from django.contrib.staticfiles import finders
11from django.core.exceptions import ImproperlyConfigured
12from django.db.models import AutoField
13from django.template import Context, loader, Template
14from django.template.exceptions import TemplateDoesNotExist
15from django.utils.module_loading import module_has_submodule
16from importlib import import_module
17from itertools import chain
18from operator import attrgetter
19from superdjango.conf import SUPERDJANGO
20import warnings
22# Exports
24__all__ = (
25 "copy_model_instance",
26 "get_app_modules",
27 "get_model",
28 "get_setting",
29 "get_setting_with_prefix",
30 "get_user_name",
31 "has_o2o",
32 "parse_string",
33 "parse_template",
34 "static_file_exists",
35 "template_exists",
36 "there_can_be_only_one",
37 "title",
38 "user_in_group",
39 # "CombinedQuerySet",
40 "CompareQuerySet",
41)
43# Constants
45DEBUG = getattr(settings, "DEBUG", False)
47# Functions
50def copy_model_instance(instance):
51 """Copy an instance of a model.
53 :param instance: The model instance to be copied.
54 :type instance: Model
56 :returns: Returns a copy of the model instance.
58 .. tip::
59 The new instance has *not* been saved to the database. Also, reference fields must be manually recreated using
60 the original instance.
62 """
63 initial_values = dict()
65 # noinspection PyProtectedMember
66 for f in instance._meta.fields:
67 # noinspection PyProtectedMember
68 if not isinstance(f, AutoField) and f not in list(instance._meta.parents.values()):
69 initial_values[f.name] = getattr(instance, f.name)
71 return instance.__class__(**initial_values)
74def get_app_modules(name):
75 """Yields tuples of ``(app_name, module)`` for each installed app that includes the given module name.
77 :param name: The module name.
78 :type name: str
80 """
81 for app in django_apps.get_app_configs():
82 if module_has_submodule(app.module, name):
83 yield app.name, import_module("%s.%s" % (app.name, name))
86def get_model(dotted_path, raise_exception=True):
87 """Get the model for the given dotted path.
89 :param dotted_path: The ``app_label.ModelName`` path of the model to return.
90 :type dotted_path: str
92 :param raise_exception: Raise an exception on failure.
93 :type raise_exception: bool
95 """
96 try:
97 return django_apps.get_model(dotted_path, require_ready=False)
98 except ValueError:
99 if raise_exception:
100 raise ImproperlyConfigured('dotted_path must be in the form of "app_label.ModelName".')
101 except LookupError:
102 if raise_exception:
103 raise ImproperlyConfigured("dotted_path refers to a model that has not been installed: %s" % dotted_path)
105 return None
108def get_setting(name, default=None):
109 """Get the value of the named setting from the ``settings.py`` file.
111 :param name: The name of the setting.
112 :type name: str
114 :param default: The default value.
116 """
117 message = "get_settings() will likely be removed in the next minor version. Use " \
118 "conf.SUPERDJANGO.get() instead."
119 warnings.warn(message, PendingDeprecationWarning)
121 return getattr(settings, name.upper(), default)
124def get_setting_with_prefix(name, default=None, prefix="SUPERDJANGO"):
125 """Get the value of the named setting from the ``settings.py`` file.
127 :param name: The name of the setting.
128 :type name: str
130 :param default: The default value.
132 :param prefix: The setting prefix.
133 :type prefix: str
135 """
136 message = "get_setting_with_prefix() will likely be removed in the next minor version. Use " \
137 "conf.SUPERDJANGO.get() instead."
138 warnings.warn(message, PendingDeprecationWarning)
140 full_name = "%s_%s" % (prefix, name.upper())
141 return get_setting(full_name, default=default)
144def get_user_name(user):
145 """Get the full name of the user, if available, or just the value of the username field if not.
147 :param user: The user instance.
149 :rtype: str | None
151 :raises: ``AttributeError`` if a custom user model has been implemented without a ``get_full_name()`` method.
153 .. tip::
154 It is safe to call this function at runtime where user is ``None``.
156 """
157 if user is None:
158 return None
160 full_name = user.get_full_name()
161 if len(full_name) > 0:
162 return full_name
164 return getattr(user, user.USERNAME_FIELD)
167def has_o2o(instance, field_name):
168 """Test whether a OneToOneField is populated.
170 :param instance: The model instance to check.
171 :type instance: object
173 :param field_name: The name of the one to one field.
174 :type field_name: str || unicode
176 :rtype: bool
178 This is the same `hasattr()``, but is more explicit and easier to remember.
180 See http://devdocs.io/django/topics/db/examples/one_to_one
182 """
183 return hasattr(instance, field_name)
186def parse_string(string, context):
187 """Parse a string as a Django template.
189 :param string: The name of the template.
190 :type string: str
192 :param context: Context variables.
193 :type context: dict
195 :rtype: str
197 """
198 message = "parse_string() has been moved to superdjango.html.shortcuts and will be removed in the next " \
199 "minor version."
200 warnings.warn(message, PendingDeprecationWarning)
202 template = Template(string)
203 return template.render(Context(context))
206def parse_template(template, context):
207 """Ad hoc means of parsing a template using Django's built-in loader.
209 :param template: The name of the template.
210 :type template: str || unicode
212 :param context: Context variables.
213 :type context: dict
215 :rtype: str
217 """
218 message = "parse_template() has been moved to superdjango.html.shortcuts and will be removed in the next " \
219 "minor version."
220 warnings.warn(message, PendingDeprecationWarning)
222 return loader.render_to_string(template, context)
225def static_file_exists(path, tokens=None):
226 """Determine whether a static file exists.
228 :param path: The path to be checked.
229 :type path: str
231 :param tokens: If given, format will be used to replace the tokens in the path.
232 :type tokens: dict
234 :rtype: bool
236 """
237 if tokens is not None:
238 path = path.format(**tokens)
240 return finders.find(path) is not None
243def template_exists(name):
244 """Indicates whether the given template exists.
246 :param name: The name (path) of the template to load.
247 :type name: str
249 :rtype: bool
251 """
252 message = "template_exists() has been moved to superdjango.html.shortcuts and will be removed in the next " \
253 "minor version."
254 warnings.warn(message, PendingDeprecationWarning)
256 try:
257 loader.get_template(name)
258 return True
259 except TemplateDoesNotExist:
260 return False
263def there_can_be_only_one(cls, instance, field_name):
264 """Helper function that ensures a given boolean field is ``True`` for only one record in the database. It is
265 intended for use with ``pre_save`` signals.
267 :param cls: The class (sender) emitting the signal.
268 :type cls: class
270 :param instance: The instance to be checked.
271 :type: instance: object
273 :param field_name: The name of the field to be checked. Must be a ``BooleanField``.
274 :type field_name: str | unicode
276 :raises: ``AttributeError`` if the ``field_name`` does not exist on the ``instance``.
278 """
279 field_value = getattr(instance, field_name)
281 if field_value:
282 cls.objects.update(**{field_name: False})
285def title(value, uppers=None):
286 """Smart title conversion.
288 :param value: The value to converted to Title Case.
289 :type value: str
291 :param uppers: A list of keywords that are alway upper case.
292 :type uppers: list[str]
294 :rtype: str
296 """
297 if uppers is None:
298 uppers = list()
300 uppers += SUPERDJANGO.UPPERS
302 tokens = value.split(" ")
303 _value = list()
304 for t in tokens:
305 if t.lower() in uppers:
306 v = t.upper()
307 else:
308 v = t.title()
310 _value.append(v)
312 return " ".join(_value)
315def user_in_group(user, group):
316 """Determine whether a given user is in a particular group.
318 :param user: The user instance to be checked.
319 :type user: User
321 :param group: The name of the group.
322 :type group: str
324 :rtype: bool
326 """
327 return user.groups.filter(name=group).exists()
330# Classes
333# TODO: Original ideas for CombinedQuerySet doesn't really work. Needs lots of thought and testing to mimic a queryset.
334# Will work on this again when I have a need for a combined qs.
335# class CombinedQuerySet(object):
336# """Combines two are more querysets as though they were one."""
337#
338# def __init__(self, querysets=None):
339# if querysets is not None:
340# self.rows = querysets
341# else:
342# self.rows = list()
343#
344# if querysets is not None:
345# self.results = list(chain(*querysets))
346# else:
347# self.results = list()
348#
349# def append(self, qs):
350# self.results.append(chain(qs))
351#
352# def filter(self, **criteria):
353# _results = list()
354# for qs in self.results:
355# new_qs = qs.filter(**criteria)
356# _results.append(new_qs)
357#
358# return CombinedQuerySet(querysets=_results)
359#
360# def order_by(self, field_name):
361# return sorted(chain(*self.results), key=attrgetter(field_name))
364class CompareQuerySet(object):
365 """Compare two querysets to see what's been added or removed."""
367 def __init__(self, qs1, qs2):
368 """Initialize the comparison.
370 :param qs1: The primary or "authoritative" queryset.
371 :type qs1: django.db.models.QuerySet
373 :param qs2: The secondary queryset.
374 :type qs2: django.db.models.QuerySet
376 """
377 self.qs1 = qs1
378 self.qs2 = qs2
380 self._pk1 = list()
381 for row in qs1:
382 self._pk1.append(row.pk)
384 self._pk2 = list()
385 for row in qs2:
386 self._pk2.append(row.pk)
388 def get_added(self):
389 """Get the primary keys of records in qs2 that do not appear in qs1.
391 :rtype: list
393 """
394 # Faster method for larger querysets?
395 # https://stackoverflow.com/a/3462160/241720
396 a = list()
397 for pk in self._pk2:
398 if pk not in self._pk1:
399 a.append(pk)
401 return a
403 def get_removed(self):
404 """Get the primary keys of records in qs2 that do not appear in qs1.
406 :rtype: list
408 """
409 # Faster method for larger querysets?
410 # https://stackoverflow.com/a/3462160/241720
411 a = list()
412 for pk in self._pk2:
413 if pk not in self._pk1:
414 a.append(pk)
416 return a
418 def is_different(self):
419 """Indicates whether the two querysets are different.
421 :rtype: bool
423 """
424 return set(self._pk1) != set(self._pk2)
426 def is_same(self):
427 """Indicates whether the two querysets are the same.
429 :rtype: bool
431 """
432 return set(self._pk1) == set(self._pk2)