Compare commits

..

7 Commits

  1. 1
      .gitignore
  2. 2
      AUTHORS
  3. 102
      CHANGES
  4. 9
      README.md
  5. 33
      setup.py
  6. 14
      shared/utils/__init__.py
  7. 172
      shared/utils/admin_actions.py
  8. 10
      shared/utils/conf.py
  9. 20
      shared/utils/dateformat.py
  10. 12
      shared/utils/daytime.py
  11. 16
      shared/utils/debugging.py
  12. 111
      shared/utils/fields.py
  13. 2
      shared/utils/forms.py
  14. 9
      shared/utils/functional.py
  15. 0
      shared/utils/locale/de/__init__.py
  16. 32
      shared/utils/locale/de/formats.py
  17. 0
      shared/utils/locale/en/__init__.py
  18. 36
      shared/utils/locale/en/formats.py
  19. 4
      shared/utils/management/commands/fix_proxymodel_permissions.py
  20. 122
      shared/utils/models/events.py
  21. 130
      shared/utils/models/pages.py
  22. 173
      shared/utils/models/slugs.py
  23. 173
      shared/utils/models/workflow.py
  24. 25
      shared/utils/preview.py
  25. 30
      shared/utils/templates/admin/action_forms/admin_action_base.html
  26. 2
      shared/utils/templates/utils/_language_switcher_get.html
  27. 8
      shared/utils/templates/utils/_text_list.html
  28. 11
      shared/utils/templatetags/daterange.py
  29. 24
      shared/utils/templatetags/debug_utils.py
  30. 93
      shared/utils/templatetags/text_tags.py
  31. 28
      shared/utils/templatetags/translation_tags.py
  32. 29
      shared/utils/templatetags/view_helpers.py
  33. 116
      shared/utils/text.py
  34. 14
      shared/utils/timezone.py
  35. 130
      shared/utils/translation.py
  36. 33
      shared/utils/url_helpers.py
  37. 0
      shared/utils/views/__init__.py
  38. 48
      shared/utils/views/alphabetical_pagination.py
  39. 553
      shared/utils/views/daterange.py

1
.gitignore vendored

@ -58,4 +58,3 @@ docs/_build/
# PyBuilder
target/
_version.py

2
AUTHORS

@ -1,2 +0,0 @@
Erik Stein <erik@classlibrary.net>, 2008-
Jan Gerber <j@thing.net>

102
CHANGES

@ -1,102 +0,0 @@
0.2.31 2020-12-10
- Use "transliterate" alias for "translit/long" codec (Python 3.9 codecs compatibility)
0.2.30 2020-03-27
- Added strip_links template filter.
0.2.29 2019-09-05
- is_past and is_future template filters.
- LONG_DATE_FORMAT.
0.2.28 2019-07-19
- Fixed last of month calculation.
0.2.27 2019-06-19
- Deprecated get_short_title, use get_short_name.
0.2.26 2019-04-23
- Updated template class ussage in I18nDirectTemplateView.
- Removed (non-working) i18n_direct_to_template view function.
0.2.25 2019-04-17
- GET language switcher code generalized.
- Import reverse from django.urls.
0.2.24 2019-03-18
- SlugField: Leave empty slug if blank=True.
- SlugField: Don't force None to "none" string.
0.2.23 2019-03-18
- lang_suffix: 'field_name' paramter instead of 'fieldname'.
0.2.22 2019-02-20
- PageTitlesFunctionMixin: __str__ without HTML.
0.2.21 2019-02-19
- Mock datetime for preview.
- Class based daterange views.
0.2.20 2019-01-15
- select_template filter.
0.2.19 2019-01-31
- Added switch_language_url template tag.
0.2.18 2019-01-28
- Added dispatch_slug_path.
- Added AdminActionBase, TargetActionBase.
0.2.17 2018-12-17
- PageTitlesMixin: Slimdown name for get_short_title.
- Daytime utils.
- Debugging utils.
- Load correct translations tags in language switcher fragment.
0.2.16 2018-11-21
- Improved get_runtime_display.
0.2.15 2018-11-21
- Respect USE_TRANSLATABLE_FIELDS setting.
- view_helpers template tags.
0.2.14 2018-11-15
- lang parameter for time_format, date_format
- _text_list.html without unnecessary whitespace
- Separated fields from methods in RuntimeMixin
0.2.13 2018-10-13
- Improved USE_TRANSLATABLE_FIELDS
- Option to allow empty runtimes (without even a start date)
- SlugTreeMixin: Check `has_url` and `parent` fields, too
- PageTitlesMixin: Renamed `short_title`-field to `name`
0.2.12 2018-09-28
- Additional text template tags.
0.2.11 2018-09-27
- RichTextBase has no longer StyleMixin as default.
0.2.10 2018-09-27
- Fix in PageTitlesFunctionMixin
0.2.8 2018-09-21
- get_admin_url in debug_utils
0.2.7 2018-09-04
- Make sure the slug field is never empty.
- Allow function in AutoSlugfield populate_from.
0.2.6
- Added missing requirement for django-dirtyfields
0.2.5
- Improved slug functions, slug fields (DowngradingSlugField).
- Move location of SLUG_HELP, DEFAULT_SLUG and AutoSlugField, now in shared.utils.models.slugs.
- Added SlugTreeMixin and helper signal functions.
- __str__ function for PageTitlesMixin
0.2.4
- Added RuntimeMixin.
0.2.3
- Added PageTitlesFunctionMixin.

9
README.md

@ -1,12 +1,3 @@
# django-shared-utils
Mix of Python and Django utility functions, classed etc.
To enable the needed custom date formats add
FORMAT_MODULE_PATH = [
'shared.utils.locale',
]
to your settings file.

33
setup.py

@ -1,39 +1,14 @@
#!/usr/bin/env python
from io import open
from setuptools import find_packages, setup
import os
import re
import subprocess
"""
Use `git tag -a -m "Release 1.0.0" 1.0.0` to tag a release;
`python setup.py --version` to update the _version.py file.
"""
from setuptools import setup, find_packages
def get_version(prefix):
if os.path.exists('.git'):
parts = subprocess.check_output(['git', 'describe', '--tags']).decode().strip().split('-')
if len(parts) == 3:
version = '{}.{}+{}'.format(*parts)
else:
version = parts[0]
version_py = "__version__ = '{}'".format(version)
_version = os.path.join(prefix, '_version.py')
if not os.path.exists(_version) or open(_version).read().strip() != version_py:
with open(_version, 'w') as fd:
fd.write(version_py)
return version
else:
for f in ('_version.py', '__init__.py'):
f = os.path.join(prefix, f)
if os.path.exists(f):
with open(f) as fd:
import re
with open(os.path.join(prefix, '__init__.py')) as fd:
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", fd.read()))
if 'version' in metadata:
break
return metadata['version']
@ -63,8 +38,6 @@ setup(
'python-dateutil',
'beautifulsoup4',
'translitcodec',
'django-dirtyfields',
'six',
],
classifiers=[
# 'Development Status :: 4 - Beta',

14
shared/utils/__init__.py

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2007-2016
__version__ = '0.2.1'
VERSION = tuple(int(d) for d in __version__.split('.'))
try:
from ._version import __version__
except ImportError:
__version__ = '0.0.0+see-git-tag'
try:
from django.utils.translation import ugettext_lazy as _
VERSION = __version__.split('+')
VERSION = tuple(list(map(int, VERSION[0].split('.'))) + VERSION[1:])
SLUG_HELP = _("Kurzfassung des Namens für die Adresszeile im Browser. Vorzugsweise englisch, keine Umlaute, nur Bindestrich als Sonderzeichen.")
except ImportError:
pass

172
shared/utils/admin_actions.py

@ -1,172 +0,0 @@
from django import forms
from django.contrib import admin
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import ngettext, gettext_lazy as _
class AdminActionBase:
action_name = None
options_template_name = 'admin/action_forms/admin_action_base.html'
title = None
queryset_action_label = None
action_button_label = None
def __init__(self, action_name=None):
if action_name:
self.action_name = action_name
def apply(self, queryset, form):
raise NotImplementedError
def get_message(self, count):
raise NotImplementedError
def get_failure_message(self, count, failure_count):
raise NotImplementedError
class BaseForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
def get_form_class(self, modeladmin, request, queryset):
"""
Example:
class CustomForm(BaseForm)
chosen_target = forms.ModelChoiceField(
label=_("Choose target itembundle"),
queryset=ItemBundle.objects.exclude(pk__in=queryset),
widget=ForeignKeyRawIdWidget(modeladmin.model._meta.get_field('parent').rel, modeladmin.admin_site),
empty_label=_("Root level"), required=False)
return CustomForm
"""
raise NotImplementedError
def __call__(self, modeladmin, request, queryset):
form_class = self.get_form_class(modeladmin, request, queryset)
form = None
if 'apply' in request.POST:
form = form_class(request.POST)
if form.is_valid():
queryset_count = queryset.count()
count = self.apply(queryset, form)
failure_count = queryset_count - count
if failure_count > 0:
message = self.get_failure_message(form, count, failure_count)
else:
message = self.get_message(form, count)
modeladmin.message_user(request, message)
return HttpResponseRedirect(request.get_full_path())
if 'cancel' in request.POST:
return HttpResponseRedirect(request.get_full_path())
if not form:
form = form_class(initial={
'_selected_action': request.POST.getlist(
admin.ACTION_CHECKBOX_NAME),
})
return render(request, self.options_template_name, context={
'action_name': self.action_name,
'title': self.title,
'queryset_action_label': self.queryset_action_label,
'action_button_label': self.action_button_label,
'queryset': queryset,
'action_form': form,
'opts': modeladmin.model._meta,
})
class TargetActionBase(AdminActionBase):
target_model = None
related_field_name = None
def get_form_class(self, modeladmin, request, queryset):
class ChooseTargetForm(AdminActionBase.BaseForm):
chosen_target = forms.ModelChoiceField(
label=_("Choose {}".format(self.target_model._meta.verbose_name)),
queryset=self.target_model.objects.exclude(pk__in=queryset),
widget=ForeignKeyRawIdWidget(
modeladmin.model._meta.get_field(self.related_field_name).rel,
modeladmin.admin_site
),
)
return ChooseTargetForm
def get_target(self, form):
return form.cleaned_data['chosen_target']
def get_message(self, form, count):
chosen_target = form.cleaned_data['chosen_target']
target_name = chosen_target.name
return ngettext(
'Successfully added %(count)d %(verbose_name)s to %(target)s.',
'Successfully added %(count)d %(verbose_name_plural)s to %(target)s.',
count) % {
'count': count,
'verbose_name': self.target_model._meta.verbose_name,
'verbose_name_plural': self.target_model._meta.verbose_name_plural,
'target': target_name}
def get_failure_message(self, form, count, failure_count):
chosen_target = form.cleaned_data['chosen_target']
target_name = chosen_target.name
return ngettext(
'Adding %(count)d %(verbose_name)s to %(target)s, %(failure_count)s failed or skipped.',
'Adding %(count)d %(verbose_name_plural)s to %(target)s, %(failure_count)s failed or skipped.',
count) % {
'count': count,
'verbose_name': self.target_model._meta.verbose_name,
'verbose_name_plural': self.target_model._meta.verbose_name_plural,
'target': target_name,
'failure_count': failure_count}
class ChangeForeignKeyAction(AdminActionBase):
# Subclass::
# action_name = 'change_fieldname_action'
# field_name_label = _("<field name>")
# title = _("Change <field name>")
# queryset_action_label = _("For the following items the <field name> will be changed:")
# action_button_label = _("Change <field name>")
def apply(self, queryset, form):
raise NotImplementedError("apply must be implemented by the subclass.")
# Subclass::
# new_value = form.cleaned_data['new_value']
# return change_fieldname_state(queryset, new_value)
def get_value_choices_queryset(self, modeladmin, request, queryset):
raise NotImplementedError("get_value_choices_queryset must be implemented by the subclass.")
def get_form_class(self, modeladmin, request, queryset):
class ChooseValueForm(AdminActionBase.BaseForm):
new_value = forms.ModelChoiceField(
label=_("Choose %(field_name)s" % {'field_name': self.field_name_label}),
queryset=self.get_value_choices_queryset(modeladmin, request, queryset),
required=True)
return ChooseValueForm
def get_message(self, form, count):
new_value = form.cleaned_data['new_value']
return ngettext(
'Successfully changed %(field_name)s of %(count)d item to %(new_value)s.',
'Successfully changed %(field_name)s of %(count)d items to %(new_value)s.',
count) % {
'field_name': self.field_name_label,
'count': count,
'new_value': new_value}
def get_failure_message(self, form, count, failure_count):
new_value = form.cleaned_data['new_value']
return ngettext(
'Successfully changed %(field_name)s of %(count)d item to %(new_value)s, %(failure_count)s failed or skipped.',
'Successfully changed %(field_name)s of %(count)d items to %(new_value)s, %(failure_count)s failed or skipped.',
count) % {
'field_name': self.field_name_label,
'count': count,
'new_value': new_value,
'failure_count': failure_count}

10
shared/utils/conf.py

@ -1,10 +0,0 @@
from django.conf import settings
USE_TRANSLATABLE_FIELDS = (
getattr(settings, 'CONTENT_PLUGINS_USE_TRANSLATABLE_FIELDS', False) or
getattr(settings, 'USE_TRANSLATABLE_FIELDS', False)
)
USE_PREVIEW_DATETIME = getattr(settings, 'USE_PREVIEW_DATETIME', False)
# You also have to set PREVIEW_DATETIME = datetime(...)

20
shared/utils/dateformat.py

@ -15,8 +15,8 @@ import re
from django.conf import settings
from django.utils.dateformat import DateFormat, re_escaped
from django.utils.formats import get_format
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
# All get_format call make sure that there is a language code returned
# (our get_language at least returns FALLBACK_LANGUAGE_CODE), because self-defined
@ -45,9 +45,9 @@ class ExtendedFormat(DateFormat):
def format(self, formatstr):
pieces = []
for i, piece in enumerate(re_formatchars.split(force_str(formatstr))):
for i, piece in enumerate(re_formatchars.split(force_text(formatstr))):
if i % 2:
pieces.append(force_str(getattr(self, piece)()))
pieces.append(force_text(getattr(self, piece)()))
elif piece:
pieces.append(re_escaped.sub(r'\1', piece))
return ''.join(pieces)
@ -59,19 +59,15 @@ def format(value, format):
return df.format(format)
def time_format(value, format=None, use_l10n=None, lang=None):
def time_format(value, format=None, use_l10n=None):
# Copy of django.utils.dateformat.time_format, using our extended formatter
if not lang:
lang = get_language()
tf = ExtendedFormat(value)
return tf.format(get_format(format or 'DATE_FORMAT', use_l10n=use_l10n, lang=lang))
return tf.format(get_format(format or 'DATE_FORMAT', use_l10n=use_l10n, lang=get_language()))
def date_format(value, format=None, use_l10n=None, lang=None):
if not lang:
lang = get_language()
def date_format(value, format=None, use_l10n=None):
df = ExtendedFormat(value)
return df.format(get_format(format or 'DATE_FORMAT', use_l10n=use_l10n, lang=lang))
return df.format(get_format(format or 'DATE_FORMAT', use_l10n=use_l10n, lang=get_language()))
def _normalize_variant(variant):

12
shared/utils/daytime.py

@ -1,12 +0,0 @@
from django.utils import timezone
def midnight(dt=None):
"""
Returns upcoming midnight.
"""
if not dt:
dt = timezone.now()
return dt.replace(hour=0, minute=0, second=0, microsecond=0) + \
timezone.timedelta(days=1)

16
shared/utils/debugging.py

@ -1,16 +0,0 @@
import logging
from django.template import TemplateDoesNotExist
from django.template.loader import select_template
logger = logging.getLogger(__name__)
def log_select_template(template_names):
logger.info("\nPossible template names:")
logger.info("\n".join(template_names))
try:
logger.info("Chosen: %s" % select_template(template_names).template.name)
except TemplateDoesNotExist:
logger.warn(" Could not find a matching template file.")

111
shared/utils/fields.py

@ -1,59 +1,84 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2008-2015
import re
from django.db.models import fields
from django.utils import six
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
if six.PY3:
from functools import reduce
from .text import slugify
from .text import slugify_long as slugify
from . import SLUG_HELP
def uniquify_field_value(instance, field_name, value, max_length=None, queryset=None):
"""
Makes a char field value unique by appending an index, taking care of the
field's max length.
DEFAULT_SLUG = "item"
FIXME Doesn't work with model inheritance, where the field is part of the parent class.
def unique_slug(instance, slug_field, slug_value, max_length=50, queryset=None):
"""
TODO Doesn't work with model inheritance, where the slug field is part of the parent class.
"""
if not slug_value:
raise ValueError("Cannot uniquify empty slug")
orig_slug = slug = slugify(force_text(slug_value))
index = 0
if not queryset:
queryset = instance._meta.default_manager.get_queryset()
queryset = instance.__class__._default_manager.get_queryset()
field = instance._meta.get_field(field_name)
if not max_length:
max_length = instance._meta.get_field(field_name).max_length
if not value:
if field.blank:
# Special case: Make sure only one row has ean empty value
if queryset.exclude(pk=instance.pk).filter(**{field_name: ''}).exists():
raise ValueError("Only one blank (root) entry allowed.")
return
else:
raise ValueError("Cannot uniquify empty value")
# TODO Instead get value from instance.field, or use a default value?
def get_similar_values(value):
def get_similar_slugs(slug):
return queryset.exclude(pk=instance.pk) \
.filter(**{"%s__istartswith" % field_name: value}).values_list(field_name, flat=True)
.filter(**{"%s__istartswith" % slug_field: slug}).values_list(slug_field, flat=True)
# Find already existing counter
m = re.match(r'(.+)(-\d+)$', value)
if m:
base_value, counter = m.groups()
index = int(counter.strip("-")) + 1
else:
base_value = value
index = 2 # Begin appending "-2"
similar_values = get_similar_values(value)
while value in similar_values or len(value) > max_length:
value = "%s-%i" % (base_value, index)
if len(value) > max_length:
base_value = base_value[:-(len(value) - max_length)]
value = "%s-%i" % (base_value, index)
similar_values = get_similar_values(base_value)
similar_slugs = get_similar_slugs(slug)
while slug in similar_slugs or len(slug) > max_length:
index += 1
return value
slug = "%s-%i" % (orig_slug, index)
if len(slug) > max_length:
orig_slug = orig_slug[:-(len(slug) - max_length)]
slug = "%s-%i" % (orig_slug, index)
similar_slugs = get_similar_slugs(orig_slug)
return slug
# TODO Remove alias
def unique_slug(instance, slug_field, slug_value, max_length=50, queryset=None):
slug_value = slugify(slug_value)
return uniquify_field_value(instance, slug_field, slug_value, max_length=50, queryset=None)
def unique_slug2(instance, slug_source, slug_field):
slug = slugify(slug_source)
all_slugs = [sl.values()[0] for sl in instance.__class__._default_manager.values(slug_field)]
if slug in all_slugs:
counter_finder = re.compile(r'-\d+$')
counter = 2
slug = "%s-%i" % (slug, counter)
while slug in all_slugs:
slug = re.sub(counter_finder, "-%i" % counter, slug)
counter += 1
return slug
class AutoSlugField(fields.SlugField):
# AutoSlugField based on http://www.djangosnippets.org/snippets/728/
def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 50)
kwargs.setdefault('help_text', SLUG_HELP)
if 'populate_from' in kwargs:
self.populate_from = kwargs.pop('populate_from')
self.unique_slug = kwargs.pop('unique_slug', False)
super(AutoSlugField, self).__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
value = getattr(model_instance, self.attname)
if not value:
if hasattr(self, 'populate_from'):
# Follow dotted path (e.g. "occupation.corporation.name")
value = reduce(lambda obj, attr: getattr(obj, attr), self.populate_from.split("."), model_instance)
if callable(value):
value = value()
if not value:
value = DEFAULT_SLUG
if self.unique_slug:
return unique_slug(model_instance, self.name, value, max_length=self.max_length)
else:
return slugify(value)

2
shared/utils/forms.py

@ -4,7 +4,7 @@ from __future__ import unicode_literals
from django import forms
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
# from http://stackoverflow.com/questions/877723/inline-form-validation-in-django#877920

9
shared/utils/functional.py

@ -1,3 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2017
def firstof(*args, default=None):
"""
Returns the first value which is neither empty nor None.
@ -12,7 +17,3 @@ def firstof(*args, default=None):
if value:
return value
return default
def join_existing(delimiter, *items):
return delimiter.join([str(i) for i in items if i])

0
shared/utils/locale/de/__init__.py

32
shared/utils/locale/de/formats.py

@ -1,32 +0,0 @@
DATETIME_FORMAT = 'j. F Y H:i'
SHORT_YEAR_FORMAT = 'Y'
SHORT_MONTH_FORMAT = 'b'
SHORT_DAY_FORMAT = 'j.'
SHORT_DAY_MONTH_FORMAT = 'j.n.'
SHORT_YEAR_MONTH_FORMAT = 'n/Y'
SHORT_DATE_FORMAT = 'j.n.Y'
SHORT_TIME_FORMAT = 'q'
LONG_DATE_FORMAT = 'l, j. F Y'
YEAR_FORMAT = 'Y'
MONTH_FORMAT = 'F'
DAY_FORMAT = 'j.'
DAY_MONTH_FORMAT = 'j. F'
# from Django: YEAR_MONTH_FORMAT
SHORT_DAYONLY_FORMAT = SHORT_DAY_FORMAT # FIXME Deprecated
SHORT_DAYMONTH_FORMAT = SHORT_DAY_MONTH_FORMAT # FIXME Deprecated
DAYONLY_FORMAT = DAY_FORMAT # FIXME Deprecated
DAYMONTH_FORMAT = DAY_MONTH_FORMAT # FIXME Deprecated
DATE_RANGE_SEPARATOR = ''
OPENING_HOURS_DATE_FORMAT = 'D j. F'
OPENING_HOURS_TIME_FORMAT = 'q'

0
shared/utils/locale/en/__init__.py

36
shared/utils/locale/en/formats.py

@ -1,36 +0,0 @@
# UK Style Date Format
DATETIME_FORMAT = 'j F Y H:i'
SHORT_YEAR_FORMAT = 'Y'
SHORT_MONTH_FORMAT = 'b'
SHORT_DAY_FORMAT = 'd'
SHORT_DAY_MONTH_FORMAT = 'd/n'
SHORT_YEAR_MONTH_FORMAT = 'n/Y'
SHORT_DATE_FORMAT = 'd/n/Y'
SHORT_TIME_FORMAT = 'q'
LONG_DATE_FORMAT = 'l, j. F Y'
YEAR_FORMAT = 'Y'
MONTH_FORMAT = 'F'
DAY_FORMAT = 'j'
DAY_MONTH_FORMAT = 'j F'
DATE_FORMAT = 'j F Y'
# from Django: YEAR_MONTH_FORMAT
SHORT_DAYONLY_FORMAT = SHORT_DAY_FORMAT # FIXME Deprecated
SHORT_DAYMONTH_FORMAT = SHORT_DAY_MONTH_FORMAT # FIXME Deprecated
DAYONLY_FORMAT = DAY_FORMAT # FIXME Deprecated
DAYMONTH_FORMAT = DAY_MONTH_FORMAT # FIXME Deprecated
DATE_RANGE_SEPARATOR = ''
OPENING_HOURS_DATE_FORMAT = 'D j F'
OPENING_HOURS_TIME_FORMAT = 'q'

4
shared/utils/management/commands/fix_proxymodel_permissions.py

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
"""Add permissions for proxy model.
This is needed because of the bug https://code.djangoproject.com/ticket/11154
in Django (as of 1.6, it's not fixed).
When a permission is created for a proxy model, it gets actually created for the
When a permission is created for a proxy model, it actually creates if for it's
base model app_label (eg: for "article" instead of "about", for the About proxy
model).

122
shared/utils/models/events.py

@ -1,122 +0,0 @@
import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from ..conf import USE_TRANSLATABLE_FIELDS
from ..dateformat import format_partial_date, format_date_range
from ..dates import get_last_of_month
if USE_TRANSLATABLE_FIELDS:
from shared.multilingual.utils.fields import TranslatableCharField
else:
TranslatableCharField = models.CharField
# FIXME Currently Python cannot handle BC dates
# Possible solution: https://github.com/okfn/datautil/blob/master/datautil/date.py
MIN_DATE = datetime.date.min
MAX_DATE = datetime.date.max
class RuntimeBehaviour:
"""
Allows a model to have partially defined from-/to-dates;
at least one year value must be entered.
"""
start_date_field_name = '_from_sort_date'
end_date_field_name = '_until_sort_date'
allow_empty_runtime = False
def clean(self):
if not self.allow_empty_runtime and not (self.from_year_value or self.until_year_value):
raise ValidationError(_('Please enter either a from or an until date year.'))
# Update from/sort date fields
if self.from_year_value:
setattr(self, self.start_date_field_name, datetime.date(
self.from_year_value, self.from_month_value or 1, self.from_day_value or 1))
else:
setattr(self, self.start_date_field_name, MIN_DATE)
if self.until_year_value:
d = datetime.date(self.until_year_value, self.until_month_value or 12, 1)
setattr(self, self.end_date_field_name, get_last_of_month(d))
else:
setattr(self, self.end_date_field_name, MAX_DATE)
def save(self, *args, **kwargs):
self.full_clean()
super(RuntimeBehaviour, self).save(*args, **kwargs)
@property
def from_date(self):
return getattr(self, self.start_date_field_name)
@property
def until_date(self):
return getattr(self, self.end_date_field_name)
# TODO ? Implement @from_date.setter, @until_date.setter
def get_from_display(self):
return self.runtime_text or format_partial_date(
self.from_year_value,
self.from_month_value,
self.from_day_value)
get_from_display.admin_order_field = start_date_field_name
get_from_display.short_description = _("from")
def get_until_display(self):
if self.runtime_text:
return ""
else:
return format_partial_date(
self.until_year_value,
self.until_month_value,
self.until_day_value)
get_until_display.admin_order_field = end_date_field_name
get_until_display.short_description = _("until")
def get_runtime_display(self):
# TODO Improve
if self.runtime_text:
return self.runtime_text
elif self.from_day_value and self.from_month_value:
return format_date_range(self.from_date, self.until_date)
else:
f = self.get_from_display()
# Single point
if self.from_date == self.until_date:
return f
u = self.get_until_display()
if f and u and not f == u:
return "{}{}".format(f, u)
else:
return f or u
class RuntimeMixin(RuntimeBehaviour, models.Model):
from_year_value = models.PositiveIntegerField(_("starting year"), null=True, blank=True)
from_month_value = models.PositiveIntegerField(_("starting month"), null=True, blank=True)
from_day_value = models.PositiveIntegerField(_("starting day"), null=True, blank=True)
until_year_value = models.PositiveIntegerField(_("ending year"), null=True, blank=True)
until_month_value = models.PositiveIntegerField(_("ending month"), null=True, blank=True)
until_day_value = models.PositiveIntegerField(_("ending day"), null=True, blank=True)
_from_sort_date = models.DateField(_("from"), editable=False)
_until_sort_date = models.DateField(_("until"), editable=False)
runtime_text = TranslatableCharField(_("Zeitangabe Textform"),
max_length=200, null=True, blank=True,
help_text=_("Alternativer Text für die Laufzeitangabe"))
start_date_field_name = '_from_sort_date'
end_date_field_name = '_until_sort_date'
allow_empty_runtime = False
class Meta:
abstract = True

130
shared/utils/models/pages.py

@ -1,116 +1,64 @@
from django.conf import settings
from django.db import models
from django.utils.html import strip_tags
from django.utils.text import normalize_newlines, Truncator
from django.utils.translation import gettext_lazy as _
from shared.multilingual.utils import i18n_fields_list
from ..functional import firstof
from ..text import slimdown
from .slugs import DowngradingSlugField
from ..conf import USE_TRANSLATABLE_FIELDS
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2017
if USE_TRANSLATABLE_FIELDS:
from shared.multilingual.utils.fields import (
TranslatableCharField,
TranslatableTextField
)
# TODO populate_from should use settings.LANGUAGE_CODE
# FIXME Wrong spelling!
SLUG_POPULATE_FROM = getattr(settings, 'SLUG_POPULATE_FROM', 'name_en')
else:
TranslatableCharField = models.CharField
TranslatableTextField = models.TextField
SLUG_POPULATE_FROM = 'name'
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.text import normalize_newlines
from django.utils.translation import ugettext_lazy as _
# TODO Make slimdown optional through settings
# TODO Leave window_title alone, do not slimdown
from ..fields import AutoSlugField
from ..functional import firstof
class PageTitlesBehaviour:
# TODO Use translatable fields by default
@python_2_unicode_compatible
class PageTitlesMixin(models.Model):
"""
Implements fallback behaviour.
A model mixin containg title and slug field for models serving as website
pages with an URL.
"""
short_title = models.CharField(_("Name"), max_length=50)
slug = AutoSlugField(_("URL-Name"), max_length=200, populate_from='short_title', unique_slug=True)
title = models.TextField(_("Titel (Langform)"), null=True, blank=True, max_length=300)
window_title = models.CharField(_("Fenster-/Suchmaschinentitel"), null=True, blank=True, max_length=300)
class Meta:
abstract = True
def __str__(self):
return strip_tags(slimdown(self.get_short_name()))
return self.short_title
def get_title(self):
return self.get_first_title_line() or \
self.name
return firstof(
self.title,
self.short_title
)
def get_short_name(self):
return getattr(self, 'short_name', '') or \
Truncator(self.name).words(5, truncate="...")
def get_window_title(self):
return firstof(
self.window_title,
self.short_title,
self.get_first_title_line(),
)
def get_first_title_line(self):
"""
First line of title field or self.name..
First line of title field.
"""
return normalize_newlines(getattr(self, 'long_title', '') or '').partition("\n")[0]
return normalize_newlines(self.get_title()).partition("\n")[0]
def get_subtitle_lines(self):
"""
All but first line of the long title field.
"""
return normalize_newlines(getattr(self, 'long_title', '') or '').partition("\n")[2]
def get_window_title(self):
return firstof(
getattr(self, 'window_title', None),
strip_tags(slimdown(self.get_short_name())),
strip_tags(slimdown(self.get_first_title_line())),
)
PageTitlesFunctionMixin = PageTitlesBehaviour
class PageTitlesMixin(PageTitlesBehaviour, models.Model):
"""
A model mixin containg title and slug fields for models serving
as web pages with an URL.
name: Main naming field, all other fields besides `slug`
are optional.
short_name: For menus etc.
long_title: Long title, composed of title line (first line) and
subtitle (all other lines)
window_title: Browser window title, also for search engine's results
slug: URL name
"""
name = TranslatableCharField(_("Name"),
max_length=250)
sort_name = TranslatableCharField(_("Short Name"),
max_length=250, null=True, blank=True,
help_text=_("Optional, used for sorting."))
short_name = TranslatableCharField(_("Short Name"),
max_length=25, null=True, blank=True,
help_text=_("Optional, used for menus etc."))
long_title = TranslatableTextField(_("title/subtitle"),
null=True, blank=True, max_length=500,
help_text=_("Optional, long title for page content region. FIrst line is the title, other lines are the subtitle. Simplified Markdown."))
window_title = TranslatableCharField(_("window title"),
null=True, blank=True, max_length=300)
slug = DowngradingSlugField(_("URL-Name"), max_length=200,
populate_from=SLUG_POPULATE_FROM, unique_slug=True, blank=True)
class Meta:
abstract = True
ordering = ['sort_name', 'name']
return normalize_newlines(self.title).partition("\n")[2]
class PageTitlesAdminMixin:
list_display = ['name', 'slug']
search_fields = ['name', 'short_name', 'long_title', 'window_title']
if USE_TRANSLATABLE_FIELDS:
search_fields = i18n_fields_list(search_fields)
class PageTitleAdminMixin(object):
search_fields = ['short_title', 'title', 'window_title']
list_display = ['short_title', 'slug']
prepopulated_fields = {
'slug': [SLUG_POPULATE_FROM]
'slug': ('short_title',),
}

173
shared/utils/models/slugs.py

@ -1,173 +0,0 @@
from functools import reduce
from django.conf import settings
from django.core import validators
from django.db import models
from django.db.models import fields as django_fields
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from dirtyfields import DirtyFieldsMixin
from ..text import slugify, downgrading_slugify
# TODO: Use english
SLUG_HELP = _("Kurzfassung des Namens für die Adresszeile im Browser. Vorzugsweise englisch, keine Umlaute, nur Bindestrich als Sonderzeichen.")
slug_re = validators._lazy_re_compile(r'^[-a-z0-9]+\Z')
validate_downgraded_slug = validators.RegexValidator(
slug_re,
_("Enter a valid 'slug' consisting of lower-case letters, numbers or hyphens."),
'invalid'
)
class AutoSlugField(django_fields.SlugField):
"""
SlugField which optionally populates the value and/or makes sure that
the value is unique. By default as stricter slugify function is used.
populate_from: Field name
unique_slug: Boolean, automatically make the field value unique
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 50)
kwargs.setdefault('help_text', SLUG_HELP)
if 'populate_from' in kwargs:
self.populate_from = kwargs.pop('populate_from')
self.unique_slug = kwargs.pop('unique_slug', False)
# FIXME Enforce unique=True
# if self.unique_slug:
# kwargs['unique'] = True
# TODO Refactor: We need a sibling FormField which also does our pre_save work and then validates correctly (called from Model.clean_fields)
super(AutoSlugField, self).__init__(*args, **kwargs)
def slugify(self, value):
return slugify(value)
def pre_save(self, model_instance, add):
value = getattr(model_instance, self.attname)
if not value:
if hasattr(self, 'populate_from'):
if callable(self.populate_from):
value = self.populate_from(model_instance, self)
else:
# Follow dotted path (e.g. "occupation.corporation.name")
value = reduce(lambda obj, attr: getattr(obj, attr),
self.populate_from.split("."), model_instance)
if callable(value):
value = value()
value = self.slugify(value)
if not value and not self.blank:
value = model_instance._meta.model_name
if self.unique_slug:
# TODO Move import to top of file once AutoSlugField is removed from shared.utils.fields and we no longer have a circular import
from ..fields import uniquify_field_value
return uniquify_field_value(
model_instance, self.name, value, max_length=self.max_length)
else:
return value
class DowngradingSlugField(AutoSlugField):
"""
SlugField which allows only lowercase ASCII characters and the dash,
automatically downgrading/replacing the entered content.
"""
default_validators = [validate_downgraded_slug]
def __init__(self, *args, **kwargs):
kwargs['allow_unicode'] = False
super(DowngradingSlugField, self).__init__(*args, **kwargs)
def slugify(self, value):
return downgrading_slugify(value)
def to_python(self, value):
# Downgrade immediately so that validators work
value = super().to_python(value)
return self.slugify(value)
def formfield(self, **kwargs):
# Remove the slug validator from the form field so that we can modify
# the field value in the model
field = super().formfield(**kwargs)
if field.default_validators:
try:
field.validators.remove(field.default_validators[0])
except ValueError:
pass
return field
class SlugTreeMixin(DirtyFieldsMixin, models.Model):
"""
Expects a `slug` and a `parent` field.
`has_url`: The node itself has an URL. It is possible that a node does
node have an URL, but its children have.
Maintains the `slug_path` field of the node and its children, watching if
either the `slug`, `parent` or `has_url` fields have changed.
"""
slug_path = models.CharField(_("URL path"),
unique=True, max_length=2000, editable=False)
# TODO Add validator to slug_path?
# validators=[
# RegexValidator(
# regex=r"^/(|.+/)$",
# message=_("Path must start and end with a slash (/)."),
# )
# ],
# TODO Make slug_path manually overridable (see feincms3 -> use_static_path)
has_url = models.BooleanField(_("has webaddress"), default=True)
FIELDS_TO_CHECK = ['slug', 'parent', 'has_url']
class Meta:
abstract = True
def _get_slug_path(self):
if self.pk:
ancestors = self.get_ancestors(include_self=False).filter(has_url=True).values_list('slug', flat=True)
parts = list(ancestors)
else:
parts = []
if self.slug:
parts += [self.slug]
return "/".join(parts)
def _rebuild_descendants_slug_path(self):
for p in self.get_descendants():
p.slug_path = p._get_slug_path()
p.save()
@receiver(pre_save)
def slug_tree_mixin_pre_save(sender, instance, **kwargs):
if isinstance(instance, SlugTreeMixin):
# FIXME: find a way to not always call this
instance.slug = instance._meta.get_field('slug').pre_save(instance, False)
instance.slug_path = instance._get_slug_path()
@receiver(post_save)
def slug_tree_mixin_post_save(sender, instance, **kwargs):
if isinstance(instance, SlugTreeMixin):
if kwargs.get('created'):
# Always get a new database instance before saving again
# or MPTTModel.save() will interpret the newly .save as
# a forbidden tree move action
# FIXME Not clear if this is a proper solution -> get rid of the slug_path stuff altogether
instance_copy = type(instance).objects.get(pk=instance.pk)
instance_copy.slug_path = instance_copy._get_slug_path()
if 'slug_path' in instance_copy.get_dirty_fields().keys():
instance_copy.save()
elif instance.get_dirty_fields().keys() & {'slug_path'}:
instance._rebuild_descendants_slug_path()

173
shared/utils/models/workflow.py

@ -1,173 +0,0 @@
from django.conf import settings
from django.contrib import admin
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
#
# Workflow Helpers
"""
States:
- inactive (either active=False or publication date not reached)
- public (active or archived)
- active (public, not archived)
- archived (public, not active)
"""
class WorkflowQuerySet(models.QuerySet):
@classmethod
def published_filter(cls):
return {
"is_published": True,
"publication_datetime__lte": timezone.now(),
}
@classmethod
def active_filter(cls):
return {
"is_published": True,
"publication_datetime__lte": timezone.now(),
"archiving_datetime__gt": timezone.now(),
}
@classmethod
def archived_filter(cls):
return {
"is_published": True,
"publication_datetime__lte": timezone.now(),
"archiving_datetime__lte": timezone.now(),
}
@classmethod
def future_filter(cls):
return {
"is_published": True,
"publication_datetime__gt": timezone.now(),
"archiving_datetime__gt": timezone.now(),
}
def unpublished(self):
return self.exclude(**self.published_filter())
def published(self):
# Active or archived
return self.filter(**self.published_filter())
public = published
def active(self):
return self.filter(**self.active_filter())
def archived(self):
return self.filter(**self.archived_filter())
def future(self):
return self.filter(**self.future_filter())
class ManyToManyWorkflowQuerySet(WorkflowQuerySet):
def _build_related_filters(self, filter_template):
"""
Transforms all filter rules to match any related models which
itself is workflow managed.
"""
filter = {}
for field in self.model._meta.get_fields():
if field.many_to_one and isinstance(field.related_model._meta.default_manager, WorkflowManager):
field.name
for path, value in filter_template.items():
filter[f'{field.name}__{path}'] = value
return filter
def published(self):
# Active or archived
return self.exclude(**self._build_related_filters(self.published_filter()))
public = published
def active(self):
return self.filter(**self._build_related_filters(self.active_filter()))
def archived(self):
return self.filter(**self._build_related_filters(self.archived_filter()))
def future(self):
return self.filter(**self._build_related_filters(self.future_filter()))
class WorkflowManager(models.Manager.from_queryset(WorkflowQuerySet)):
pass
class ManyToManyWorkflowManager(models.Manager.from_queryset(ManyToManyWorkflowQuerySet)):
pass
class WorkflowMixin(models.Model):
creation_datetime = models.DateTimeField(auto_now_add=True)
modification_datetime = models.DateTimeField(auto_now=True)
creation_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True,
editable=False,
on_delete=models.SET_NULL, related_name='+')
modification_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True,
editable=False,
on_delete=models.SET_NULL, related_name='+')
is_published = models.BooleanField(_("Published"), default=False)
publication_datetime = models.DateTimeField(_("Publication Date"), default=timezone.now)
archiving_datetime = models.DateTimeField(_("Archiving Date"), default=timezone.datetime.max)
objects = WorkflowManager()
class Meta:
abstract = True
ordering = ['-publication_datetime'] # Most recent first
@property
def workflow_status(self):
now = timezone.now()
if not self.is_published or self.publication_datetime > now:
return 'unpublished'
elif self.publication_datetime <= now and self.archiving_datetime > now:
return 'active'
else:
return 'archived'
def display_is_published(obj):
return obj.is_published and obj.publication_datetime <= timezone.now()
display_is_published.boolean = True
display_is_published.short_description = _("Aktiv")
class PublicationStateListFilter(admin.SimpleListFilter):
title = _("Published")
# Parameter for the filter that will be used in the URL query.
parameter_name = 'workflow_state'
filter_dict = {
'unpublished': 'unpublished',
'public': 'public',
'active': 'active',
'archived': 'archived',
}
def lookups(self, request, model_admin):
return (
('unpublished', _("Unpublished")),
('public', _("Published (Active or Archived)")),
('active', _("Active (published, not archived)")),
('archived', _("Archived")),
)
def queryset(self, request, queryset):
if self.value():
filter_method = self.filter_dict[self.value()]
return getattr(queryset, filter_method)()
else:
return queryset

25
shared/utils/preview.py

@ -1,25 +0,0 @@
from django.conf import settings
from django.utils import timezone
from .conf import USE_PREVIEW_DATETIME
from .timezone import smart_default_tz
class datetime(timezone.datetime):
@classmethod
def now(klass):
if USE_PREVIEW_DATETIME:
if settings.DEBUG_PREVIEW_DATETIME:
now = timezone.datetime(*settings.DEBUG_PREVIEW_DATETIME)
else:
# TODO Get preview datetime from request user
now = timezone.now()
if settings.USE_TZ:
now = smart_default_tz(now)
else:
now = timezone.now()
return now
@classmethod
def today(klass):
return klass.now().date()

30
shared/utils/templates/admin/action_forms/admin_action_base.html

@ -1,30 +0,0 @@
{% extends "admin/change_form.html" %}{# Needed for admin javascripts #}
{% load i18n static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div id="content-main">
<form action="" method="post">{% csrf_token %}
<div>
{{ action_form.as_p }}
{% for obj in queryset %}
<input type="hidden" name="_selected_action" value="{{ obj.id }}">
{% endfor %}
<p>{{ queryset_action_label }}</p>
<ul>
{{ queryset|unordered_list }}
</ul>
<input type="hidden" name="action" value="{{ action_name }}">
<input type="submit" name="apply" value="{{ action_button_label }}">
<input class="button cancel-link" type="submit" name="cancel" value="{% trans "Cancel" %}">
{# <a href="#" class="button cancel-link">{% trans "No, take me back" %}</a> #}
</div>
</form>
</div>
{% endblock %}

2
shared/utils/templates/utils/_language_switcher_get.html

@ -1,4 +1,4 @@
{% load i18n %}
{% load i18n translation_tags %}
{% get_language_info_list for LANGUAGES as languages %}

8
shared/utils/templates/utils/_text_list.html

@ -1,5 +1,9 @@
{% load i18n %}
{# List of items, where items have a (potentially empty) get_absolute_url and a name attribute #}
{# Usage: {% include "utils/_text_list.html" with items=page.get_authors no_links=True %} #}
{% for item in items %}{% if forloop.last and forloop.counter > 1 %} {% trans "and" %} {% endif %}{% with item.get_absolute_url as url %}{% if url and not no_links %}<a href="{{ url }}">{% endif %}{{ item.name }}{% if url and not no_links %}</a>{% endif %}{% if not forloop.last %}{% if forloop.revcounter0 > 1 %}, {% endif %}{% endif %}{% endwith %}{% endfor %}
{% for item in items %}
{% if forloop.last and forloop.counter > 1 %}{% trans "und" %}{% endif %}
{% with item.get_absolute_url as url %}
{% if url %}<a href="{{ url }}">{% endif %}{{ item.name }}{% if url %}</a>{% endif %}{% if not forloop.last %}{% if forloop.revcounter0 > 1 %},{% endif %}{% endif %}
{% endwith %}
{% endfor %}

11
shared/utils/templatetags/daterange.py

@ -2,7 +2,6 @@
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2016-2017
import datetime
from django import template
from django.conf import settings
@ -56,13 +55,3 @@ def format_year_range(start_date, end_date, variant=DEFAULT_VARIANT):
@register.simple_tag
def format_partial_date(year=None, month=None, day=None, variant=DEFAULT_VARIANT):
return dateformat.format_partial_date(year, month, day, variant=DEFAULT_VARIANT)
@register.filter
def is_past(_date):
return bool(_date < datetime.date.today())
@register.filter
def is_future(_date):
return bool(_date >= datetime.date.today())

24
shared/utils/templatetags/debug_utils.py

@ -1,32 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import template
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
register = template.Library()
# Erik Stein <code@classlibrary.net>, 2009-2017
try:
import ipdb
from django import template
register = template.Library()
@register.filter
def ipdb_inspect(value):
ipdb.set_trace()
return value
@register.simple_tag
def ipdb_set_breakpoint():
ipdb.set_trace()
except ImportError:
except:
pass
# TODO Move get_admin_url to different template tag library
@register.filter
def get_admin_url(obj):
content_type = ContentType.objects.get_for_model(obj.__class__)
return reverse("admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(obj.id,))

93
shared/utils/templatetags/text_tags.py

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2015
import re
import string
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.encoding import force_str
from django.utils.encoding import force_text
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
@ -22,7 +22,7 @@ def conditional_punctuation(value, punctuation=",", space=" "):
Appends punctuation if the (stripped) value is not empty
and the value does not already end in a punctuation mark (.,:;!?).
"""
value = force_str(value or "").strip()
value = force_text(value or "").strip()
if value:
if value[-1] not in ".,:;!?":
value += conditional_escape(punctuation)
@ -48,90 +48,3 @@ def nbsp(text, autoescape=True):
@stringfilter
def html_entities_to_unicode(text):
return mark_safe(text_utils.html_entities_to_unicode(text))
@register.filter(needs_autoescape=False)
def slimdown(text):
return mark_safe(text_utils.slimdown(text))
@register.filter(needs_autoescape=False)
def strip_links(text):
return mark_safe(text_utils.strip_links(text))
@register.filter(is_safe=True)
@stringfilter
def html_lines_to_list(value):
"""
Replaces all <br> tags with ", "
"""
rv = []
lines = value.split("<br>")
for i in range(0, len(lines)):
line = lines[i].strip()
rv.append(line)
if i < len(lines) - 1:
if line[-1] not in ";:,.-–—":
rv.append(", ")
else:
rv.append(" ")
return "".join(rv)
return ", ".join([l.strip() for l in value.split("<br>")])
def remove_punctuation(s):
# http://stackoverflow.com/questions/265960/best-way-to-strip-punctuation-from-a-string-in-python
# return s.translate(s.maketrans("",""), string.punctuation)
regex = re.compile('[%s]' % re.escape(string.punctuation))
return regex.sub('', s)
def splitn(s, n):
"""split string s into chunks no more than n characters long"""
parts = re.split("(.{%d,%d})" % (n, n), s)
map(parts.remove, [""] * parts.count(""))
return parts
def clean_value(value):
# Convert all whitespace including non-breaking space to a single space
if value:
return re.sub(u"([\s\u00A0]+)", u" ", force_str(value.strip()))
else:
return value
@register.filter()
@stringfilter
def append(value, text):
"""
Appends text if value is not None.
"""
if value is not None:
value = str(value).strip()
if value:
return "{}{}".format(value, text)
return ""
@register.filter()
@stringfilter
def prepend(value, text):
"""
Prepends text if value is not None.
"""
if value is not None:
value = str(value).strip()
if value:
return "{}{}".format(text, value)
return ""
@register.filter
@stringfilter
def first_line(s):
"""
Strips whitespace, then returns the first line of text.
"""
return s.strip().splitlines(False)[0]

28
shared/utils/templatetags/translation_tags.py

@ -1,9 +1,8 @@
from urllib.parse import urlsplit, urlunsplit
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2014-2015
from django import template
from django.urls.exceptions import NoReverseMatch
from django.urls import reverse
from django.utils.translation import override
from ..translation import get_translation, get_translated_field
@ -11,25 +10,6 @@ from ..translation import get_translation, get_translated_field
register = template.Library()
@register.simple_tag(takes_context=True)
def switch_language_url(context, lang_code: str):
request = context['request']
match = request.resolver_match
parsed_url = urlsplit(request.get_full_path())
to_be_reversed = "%s:%s" % (match.namespace, match.url_name) \
if match.namespace else match.url_name
with override(lang_code):
try:
url = reverse(to_be_reversed, args=match.args, kwargs=match.kwargs)
except NoReverseMatch:
pass
else:
url = urlunsplit((parsed_url.scheme, parsed_url.netloc,
url, parsed_url.query, parsed_url.fragment))
return url
@register.filter
def translation(obj):
return get_translation(obj)
@ -47,7 +27,7 @@ translated_field = translate
Unfinished:
from django import template
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.core.urlresolvers import resolve
from django.urls.base import translate_url

29
shared/utils/templatetags/view_helpers.py

@ -1,29 +0,0 @@
from urllib.parse import urlencode
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def url_replace(context, **kwargs):
query = context['request'].GET.dict()
query.update(kwargs)
return urlencode(query)
@register.filter
def paginator_context(page_range, current):
before = [p for p in page_range if p < current]
after = [p for p in page_range if p > current]
if len(before) > 3:
before = before[:2] + [''] + before[-1:]
if len(after) > 3:
after = after[:1] + [''] + after[-2:]
return before + [current] + after
@register.filter
def select_template(template_list):
return template.loader.select_template(template_list)

116
shared/utils/text.py

@ -1,14 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2015-2017
from django.utils import six
from django.utils.encoding import force_text, smart_text
from django.utils.functional import allow_lazy, keep_lazy_text
from django.utils.safestring import SafeText
from django.utils.text import slugify
from django.utils.translation import ugettext as _, ugettext_lazy
from HTMLParser import HTMLParser
import translitcodec # provides 'translit/long', used by codecs.encode()
import codecs
import html
import re
import translitcodec # provides 'translit/long', used by codecs.encode() # noqa
from django.conf import settings
from django.utils.encoding import force_str
from django.utils.functional import keep_lazy_text
from django.utils.html import mark_safe, strip_tags
from django.utils.text import slugify as django_slugify, normalize_newlines
from django.utils.translation import gettext_lazy
@keep_lazy_text
@ -16,102 +19,35 @@ def downgrade(value):
"""
Downgrade unicode to ascii, transliterating accented characters.
"""
value = force_str(value or "")
return codecs.encode(value, 'transliterate')
value = force_text(value)
return codecs.encode(value, 'translit/long')
@keep_lazy_text
def slugify_long(value):
return django_slugify(downgrade(value))
return slugify(downgrade(value))
# Spreading umlauts is included in the translit/long codec.
slugify_german = slugify_long
@keep_lazy_text
def downgrading_slugify(value):
# Slugfiy only allowing hyphens, numbers and ASCII characters
# FIXME django_slugify might return an empty string; take care that we always return something
return re.sub("[ _]+", "-", django_slugify(downgrade(value)))
SLUGIFY_FUNCTION = getattr(settings, 'SLUGIFY_FUNCTION', downgrading_slugify)
slugify = SLUGIFY_FUNCTION
@keep_lazy_text
def html_entities_to_unicode(html_str):
return html.unescape(html_str)
# Translators: Separator between list elements
DEFAULT_SEPARATOR = gettext_lazy(", ")
def html_entities_to_unicode(html):
parser = HTMLParser()
return parser.unescape(html)
html_entities_to_unicode = allow_lazy(html_entities_to_unicode, six.text_type, SafeText)
# Translators: Last separator of list elements
LAST_WORD_SEPARATOR = gettext_lazy(" and ")
# Translators: This string is used as a separator between list elements
DEFAULT_SEPARATOR = ugettext_lazy(", ")
@keep_lazy_text
def text_joined(list_, separator=DEFAULT_SEPARATOR, last_word=LAST_WORD_SEPARATOR):
def get_text_joined(list_, separator=DEFAULT_SEPARATOR, last_word=ugettext_lazy(' and ')):
list_ = list(list_)
if len(list_) == 0:
return ''
if len(list_) == 1:
return force_str(list_[0])
return force_text(list_[0])
return '%s%s%s' % (
separator.join(force_str(i) for i in list_[:-1]),
force_str(last_word), force_str(list_[-1]))
# TODO Don't match escaped stars (like \*)
b_pattern = re.compile(r"(\*\*)(.*?)\1")
i_pattern = re.compile(r"(\*)(.*?)\1")
u_pattern = re.compile(r"(__)(.*?)\1")
link_pattern = re.compile(r"\[([^\[]+)\]\(([^\)]+)\)")
@keep_lazy_text
def slimdown(text):
"""
Converts simplified markdown (`**`, `*`, `__`) to <b>, <i> und <u> tags.
"""
if text:
text, n = re.subn(b_pattern, "<b>\\2</b>", text)
text, n = re.subn(i_pattern, "<i>\\2</i>", text)
text, n = re.subn(u_pattern, "<u>\\2</u>", text)
text, n = re.subn(link_pattern, "<a href=\'\\2\'>\\1</a>", text)
return mark_safe(text)
else:
return ""
@keep_lazy_text
def strip_links(text):
return re.sub(r'<a[^>]+>', '', text, flags=re.DOTALL).replace('</a>', '')
COLLAPSE_WHITESPACE_RE = re.compile(r"\s+")
@keep_lazy_text
def collapse_whitespace(text):
return COLLAPSE_WHITESPACE_RE.sub(" ", text).strip()
@keep_lazy_text
def html_to_text(text):
print(text)
rv = collapse_whitespace(strip_tags(html_entities_to_unicode(str(text))))
print(rv)
return rv
try:
from html_sanitizer.django import get_sanitizer
def sanitized_html(html, config_name='default'):
return get_sanitizer(config_name).sanitize(html)
except ImportError:
pass
separator.join(force_text(i) for i in list_[:-1]),
force_text(last_word), force_text(list_[-1]))

14
shared/utils/timezone.py

@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2015
"""
Django and Timezones
@ -14,8 +17,6 @@ A sample Django application illustrating some time zone traps for the unwary.
"""
import datetime
from django.conf import settings
from django.utils import timezone
@ -27,12 +28,3 @@ def smart_default_tz(datetime_value):
datetime_value = timezone.make_aware(datetime_value, timezone=timezone.get_default_timezone())
return timezone.localtime(datetime_value, timezone.get_default_timezone())
def timezone_today():
"""
Return the current date in the current time zone.
"""
if settings.USE_TZ:
return timezone.localdate()
else:
return datetime.date.today()

130
shared/utils/translation.py

@ -1,14 +1,21 @@
from copy import copy
import os
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2015
from collections import OrderedDict
import os
from contextlib import contextmanager
from django import http
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist
from django.core.urlresolvers import translate_url
from django.http import HttpResponseRedirect
from django.template.loader import select_template
from django.utils import translation
from django.utils.http import is_safe_url
from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit
from django.utils.translation import check_for_language, LANGUAGE_SESSION_KEY
from django.views.generic import TemplateView
from django.views.i18n import set_language
from django.views.i18n import LANGUAGE_QUERY_PARAMETER
FALLBACK_LANGUAGE_CODE = getattr(settings, 'FALLBACK_LANGUAGE_CODE', 'en')
@ -27,28 +34,16 @@ def _normalize_language_code(language_code):
def get_language(language_code=None):
return _normalize_language_code(language_code).split("-")[0]
def get_language_order(languages=None):
"""
Returns a copy of settings.LANGUAGES with the active language at the first position.
"""
languages = languages or list(OrderedDict(settings.LANGUAGES).keys())
languages.insert(0, languages.pop(languages.index(get_language())))
return languages
return _normalize_language_code(language_code)[:2]
# TODO Deprecated 'fieldname' parameter, use 'field_name'
def lang_suffix(language_code=None, field_name=None, fieldname=None):
def lang_suffix(language_code=None):
"""
Returns the suffix appropriate for adding to field names for selecting
the current language.
If fieldname is given, returns the suffixed fieldname.
"""
language_code = _normalize_language_code(language_code or get_language()).split("-")[0]
return "{}_{}".format(field_name or fieldname or "", language_code)
language_code = _normalize_language_code(language_code)[:2]
return "_{}".format(language_code)
class DirectTemplateView(TemplateView):
@ -69,15 +64,19 @@ class I18nDirectTemplateView(DirectTemplateView):
def get_template_names(self):
t_name, t_ext = os.path.splitext(self.template_name)
lang = translation.get_language()
template = select_template([
template_name = select_template((
"%s.%s%s" % (t_name, lang, t_ext),
self.template_name
])
return [template.template.name]
)).name
return [template_name]
def i18n_direct_to_template(request, *args, **kwargs):
return I18nDirectTemplateView(*args, **kwargs).as_view()
def get_translation(obj, relation_name='translations', language_code=None):
language_code = _normalize_language_code(language_code).split("-")[0]
language_code = _normalize_language_code(language_code)[:2]
try:
return getattr(obj, relation_name).get(language=language_code)
except ObjectDoesNotExist:
@ -88,6 +87,39 @@ def get_translation(obj, relation_name='translations', language_code=None):
return None
# class FieldTranslationMixin(object):
# """
# If the model has a field `attr` or `attr_<language_code>`, return it's
# value, else raise ValueError.
# """
# def __getattr__(self, attr):
# if attr in self.__dict__:
# return self.__dict__[attr]
# for field in self._meta.multilingual:
# code = None
# match = re.match(r'^%s_(?P<code>[a-z_]{2,5})$' % field, str(attr))
# if match:
# code = match.groups('code')
# code = code[:2] # let's limit it to two letter
# elif attr in self._meta.multilingual:
# code = self._language
# field = attr
# if code is not None:
# try:
# return self._meta.translation.objects.select_related().get(model=self, language__code=code).__dict__[field]
# except ObjectDoesNotExist:
# if MULTILINGUAL_FALL_BACK_TO_DEFAULT and MULTILINGUAL_DEFAULT and code != MULTILINGUAL_DEFAULT:
# try:
# return self._meta.translation.objects.select_related().get(model=self, language__code=MULTILINGUAL_DEFAULT).__dict__[field]
# except ObjectDoesNotExist:
# pass
# if MULTILINGUAL_FAIL_SILENTLY:
# return None
# raise ValueError, "'%s' has no translation in '%s'"%(self, code)
# raise AttributeError, "'%s' object has no attribute '%s'"%(self.__class__.__name__, str(attr))
def get_translated_field(obj, field_name, language_code=None):
"""
Tries to get the model attribute corresponding to the current
@ -113,8 +145,8 @@ def get_translated_field(obj, field_name, language_code=None):
field_name + lang_suffix other language
"""
# TODO Implement multiple languages
language_code = _normalize_language_code(language_code).split("-")[0]
is_default_language = bool(language_code == settings.LANGUAGE_CODE.split("-")[0])
language_code = _normalize_language_code(language_code)[:2]
is_default_language = bool(language_code == settings.LANGUAGE_CODE[:2])
if language_code == 'de':
other_language_code = 'en'
else:
@ -154,28 +186,26 @@ def active_language(lang='de'):
def set_language_get(request):
"""
set_language per GET request,
modified copy from django.views.i18n (django 1.9.x)
"""
request = copy(request)
request.POST = request.GET
request.method = 'POST'
return set_language(request)
class I18nUrlMixin(object):
"""
View Mixin.
Makes the url pattern name available in the template context.
Usage:
class ViewClass(I18nUrlMixin, TemplateView):
...
url(r'<your_pattern>', ViewClass.as_view(view_name='my-wonderful-view', name='my-wonderful-view'),
"""
view_name = None
def get_context_data(self, **kwargs):
if 'view_name' not in kwargs and self.view_name:
kwargs['view_name'] = self.view_name
context = super().get_context_data(**kwargs)
return context
next = request.POST.get('next', request.GET.get('next'))
if not is_safe_url(url=next, host=request.get_host()):
next = request.META.get('HTTP_REFERER')
if not is_safe_url(url=next, host=request.get_host()):
next = '/'
response = http.HttpResponseRedirect(next)
if request.method == 'GET':
lang_code = request.GET.get(LANGUAGE_QUERY_PARAMETER, None)
if lang_code and check_for_language(lang_code):
next_trans = translate_url(next, lang_code)
if next_trans != next:
response = http.HttpResponseRedirect(next_trans)
if hasattr(request, 'session'):
request.session[LANGUAGE_SESSION_KEY] = lang_code
else:
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code,
max_age=settings.LANGUAGE_COOKIE_AGE,
path=settings.LANGUAGE_COOKIE_PATH,
domain=settings.LANGUAGE_COOKIE_DOMAIN)
return response

33
shared/utils/url_helpers.py

@ -1,33 +0,0 @@
from django.http import Http404
def dispatch_slug_path(*views):
"""
Dispatch full path slug in iterating through a set of views.
Http404 exceptions raised by a view lead to trying the next view
in the list.
This allows to plug different slug systems to the same root URL.
Usages::
# in urls.py
path('<slug:slug_path>/', dispatch_slug_path(
views.CategoryDetailView.as_view(),
views.ArticleDetailView.as_view())),
)
"""
def wrapper(request, **kwargs):
view_args = []
view_kwargs = {'url_path': kwargs[list(kwargs.keys())[0]]}
not_found_exception = Http404
for view in views:
try:
return view(request, *view_args, **view_kwargs)
except Http404 as e:
not_found_exception = e # assign to use it outside of except block
continue
raise not_found_exception
return wrapper

0
shared/utils/views/__init__.py

48
shared/utils/views/alphabetical_pagination.py

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db.models import F, Func, Value
class AlphabeticalPaginationMixin(object):
alphabetical_pagination_field = 'name'
def get_alphabetical_pagination_field(self):
return self.alphabetical_pagination_field
def get_selected_letter(self):
return self.request.GET.get('letter', 'a')
def get_base_queryset(self):
"""
Queryset before applying pagination filters.
"""
qs = super(AlphabeticalPaginationMixin, self).get_queryset().exclude(
**{self.get_alphabetical_pagination_field(): ''}
)
return qs
def get_queryset(self):
qs = self.get_base_queryset()
# FIXME Select Umlauts (using downgrade and also downgrade sort_name field?)
# FIXME Select on TRIM/LEFT as in get_letter_choices
filter = {
"{}__istartswith".format(self.get_alphabetical_pagination_field()):
self.get_selected_letter()}
return qs.filter(**filter).order_by(self.alphabetical_pagination_field)
def get_letter_choices(self):
return self.get_base_queryset().annotate(name_lower=Func(
Func(
Func(
F(self.get_alphabetical_pagination_field()), function='LOWER'),
function='TRIM'),
Value("1"), function='LEFT')).order_by(
'name_lower').distinct('name_lower').values_list('name_lower', flat=True)
def get_context_data(self, **kwargs):
context = super(AlphabeticalPaginationMixin, self).get_context_data(**kwargs)
context['selected_letter'] = self.get_selected_letter()
context['alphabet'] = self.get_letter_choices()
return context

553
shared/utils/views/daterange.py

@ -1,553 +0,0 @@
## UNFINISHED WORK IN PROGRESS
"""
Generic date range views.
Django's generic date views only deal with a single date per
model. The date range views replicate the API but deal with
a start and an end date.
"""
import datetime
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404
from django.utils import timezone
from django.utils.encoding import force_str, force_str
from django.utils.translation import ugettext as _
from django.views.generic.base import View
from django.views.generic.detail import (
BaseDetailView, SingleObjectTemplateResponseMixin,
)
from django.views.generic.list import (
MultipleObjectMixin, MultipleObjectTemplateResponseMixin,
)
from django.views.generic.dates import (
YearMixin, MonthMixin, DayMixin, WeekMixin,
DateMixin,
)
from .. import conf, preview
if conf.USE_PREVIEW_DATETIME:
effective_datetime = preview.datetime
else:
effective_datetime = timezone.datetime
class DateRangeMixin(DateMixin):
"""
Mixin class for views manipulating date-based data.
"""
date_field = None
end_date_field = None
allow_future = False
def get_end_date_field(self):
"""
Get the name of the end date field to be used to filter by.
"""
if self.end_date_field is None:
raise ImproperlyConfigured("%s.end_date_field is required." % self.__class__.__name__)
return self.end_date_field
# Note: the following three methods only work in subclasses that also
# inherit SingleObjectMixin or MultipleObjectMixin.
def _make_date_lookup_arg(self, value):
"""
Convert a date into a datetime when the date field is a DateTimeField.
When time zone support is enabled, `date` is assumed to be in the
current time zone, so that displayed items are consistent with the URL.
"""
if self.uses_datetime_field:
value = datetime.datetime.combine(value, datetime.time.min)
if settings.USE_TZ:
value = timezone.make_aware(value, timezone.get_current_timezone())
return value
def _make_single_date_lookup(self, date):
"""
Get the lookup kwargs for filtering on a single date.
If the date field is a DateTimeField, we can't just filter on
date_field=date because that doesn't take the time into account.
"""
date_field = self.get_date_field()
if self.uses_datetime_field:
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(date + datetime.timedelta(days=1))
return {
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
else:
# Skip self._make_date_lookup_arg, it's a no-op in this branch.
return {date_field: date}
class BaseDateListView(MultipleObjectMixin, DateMixin, View):
"""
Abstract base class for date-based views displaying a list of objects.
"""
allow_empty = False
date_list_period = 'year'
def get(self, request, *args, **kwargs):
self.date_list, self.object_list, extra_context = self.get_dated_items()
context = self.get_context_data(object_list=self.object_list,
date_list=self.date_list)
context.update(extra_context)
return self.render_to_response(context)
def get_dated_items(self):
"""
Obtain the list of dates and items.
"""
raise NotImplementedError('A DateView must provide an implementation of get_dated_items()')
def get_ordering(self):
"""
Returns the field or fields to use for ordering the queryset; uses the
date field by default.
"""
return self.get_date_field() if self.ordering is None else self.ordering
def get_dated_queryset(self, **lookup):
"""
Get a queryset properly filtered according to `allow_future` and any
extra lookup kwargs.
"""
qs = self.get_queryset().filter(**lookup)
date_field = self.get_date_field()
allow_future = self.get_allow_future()
allow_empty = self.get_allow_empty()
paginate_by = self.get_paginate_by(qs)
if not allow_future:
now = effective_datetime.now() if self.uses_datetime_field else effective_datetime.today()
qs = qs.filter(**{'%s__lte' % date_field: now})
if not allow_empty:
# When pagination is enabled, it's better to do a cheap query
# than to load the unpaginated queryset in memory.
is_empty = len(qs) == 0 if paginate_by is None else not qs.exists()
if is_empty:
raise Http404(_("No %(verbose_name_plural)s available") % {
'verbose_name_plural': force_str(qs.model._meta.verbose_name_plural)
})
return qs
def get_date_list_period(self):
"""
Get the aggregation period for the list of dates: 'year', 'month', or 'day'.
"""
return self.date_list_period
def get_date_list(self, queryset, date_type=None, ordering='ASC'):
"""
Get a date list by calling `queryset.dates/datetimes()`, checking
along the way for empty lists that aren't allowed.
"""
date_field = self.get_date_field()
allow_empty = self.get_allow_empty()
if date_type is None:
date_type = self.get_date_list_period()
if self.uses_datetime_field:
date_list = queryset.datetimes(date_field, date_type, ordering)
else:
date_list = queryset.dates(date_field, date_type, ordering)
if date_list is not None and not date_list and not allow_empty:
name = force_str(queryset.model._meta.verbose_name_plural)
raise Http404(_("No %(verbose_name_plural)s available") %
{'verbose_name_plural': name})
return date_list
class BaseArchiveIndexView(BaseDateListView):
"""
Base class for archives of date-based items.
Requires a response mixin.
"""
context_object_name = 'latest'
def get_dated_items(self):
"""
Return (date_list, items, extra_context) for this request.
"""
qs = self.get_dated_queryset()
date_list = self.get_date_list(qs, ordering='DESC')
if not date_list:
qs = qs.none()
return (date_list, qs, {})
class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView):
"""
Top-level archive of date-based items.
"""
template_name_suffix = '_archive'
class BaseYearArchiveView(YearMixin, BaseDateListView):
"""
List of objects published in a given year.
"""
date_list_period = 'month'
make_object_list = False
def get_dated_items(self):
"""
Return (date_list, items, extra_context) for this request.
"""
year = self.get_year()
date_field = self.get_date_field()
date = _date_from_string(year, self.get_year_format())
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(self._get_next_year(date))
lookup_kwargs = {
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
qs = self.get_dated_queryset(**lookup_kwargs)
date_list = self.get_date_list(qs)
if not self.get_make_object_list():
# We need this to be a queryset since parent classes introspect it
# to find information about the model.
qs = qs.none()
return (date_list, qs, {
'year': date,
'next_year': self.get_next_year(date),
'previous_year': self.get_previous_year(date),
})
def get_make_object_list(self):
"""
Return `True` if this view should contain the full list of objects in
the given year.
"""
return self.make_object_list
class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
"""
List of objects published in a given year.
"""
template_name_suffix = '_archive_year'
class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
"""
List of objects published in a given month.
"""
date_list_period = 'day'
def get_dated_items(self):
"""
Return (date_list, items, extra_context) for this request.
"""
year = self.get_year()
month = self.get_month()
date_field = self.get_date_field()
date = _date_from_string(year, self.get_year_format(),
month, self.get_month_format())
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(self._get_next_month(date))
lookup_kwargs = {
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
qs = self.get_dated_queryset(**lookup_kwargs)
date_list = self.get_date_list(qs)
return (date_list, qs, {
'month': date,
'next_month': self.get_next_month(date),
'previous_month': self.get_previous_month(date),
})
class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView):
"""
List of objects published in a given month.
"""
template_name_suffix = '_archive_month'
class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
"""
List of objects published in a given week.
"""
def get_dated_items(self):
"""
Return (date_list, items, extra_context) for this request.
"""
year = self.get_year()
week = self.get_week()
date_field = self.get_date_field()
week_format = self.get_week_format()
week_start = {
'%W': '1',
'%U': '0',
}[week_format]
date = _date_from_string(year, self.get_year_format(),
week_start, '%w',
week, week_format)
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(self._get_next_week(date))
lookup_kwargs = {
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
qs = self.get_dated_queryset(**lookup_kwargs)
return (None, qs, {
'week': date,
'next_week': self.get_next_week(date),
'previous_week': self.get_previous_week(date),
})
class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
"""
List of objects published in a given week.
"""
template_name_suffix = '_archive_week'
class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
"""
List of objects published on a given day.
"""
def get_dated_items(self):
"""
Return (date_list, items, extra_context) for this request.
"""
year = self.get_year()
month = self.get_month()
day = self.get_day()
date = _date_from_string(year, self.get_year_format(),
month, self.get_month_format(),
day, self.get_day_format())
return self._get_dated_items(date)
def _get_dated_items(self, date):
"""
Do the actual heavy lifting of getting the dated items; this accepts a
date object so that TodayArchiveView can be trivial.
"""
lookup_kwargs = self._make_single_date_lookup(date)
qs = self.get_dated_queryset(**lookup_kwargs)
return (None, qs, {
'day': date,
'previous_day': self.get_previous_day(date),
'next_day': self.get_next_day(date),
'previous_month': self.get_previous_month(date),
'next_month': self.get_next_month(date)
})
class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
"""
List of objects published on a given day.
"""
template_name_suffix = "_archive_day"
class BaseTodayArchiveView(BaseDayArchiveView):
"""
List of objects published today.
"""
def get_dated_items(self):
"""
Return (date_list, items, extra_context) for this request.
"""
return self._get_dated_items(datetime.date.today())
class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView):
"""
List of objects published today.
"""
template_name_suffix = "_archive_day"
class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
"""
Detail view of a single object on a single date; this differs from the
standard DetailView by accepting a year/month/day in the URL.
"""
def get_object(self, queryset=None):
"""
Get the object this request displays.
"""
year = self.get_year()
month = self.get_month()
day = self.get_day()
date = _date_from_string(year, self.get_year_format(),
month, self.get_month_format(),
day, self.get_day_format())
# Use a custom queryset if provided
qs = self.get_queryset() if queryset is None else queryset
if not self.get_allow_future() and date > datetime.date.today():
raise Http404(_(
"Future %(verbose_name_plural)s not available because "
"%(class_name)s.allow_future is False."
) % {
'verbose_name_plural': qs.model._meta.verbose_name_plural,
'class_name': self.__class__.__name__,
})
# Filter down a queryset from self.queryset using the date from the
# URL. This'll get passed as the queryset to DetailView.get_object,
# which'll handle the 404
lookup_kwargs = self._make_single_date_lookup(date)
qs = qs.filter(**lookup_kwargs)
return super(BaseDetailView, self).get_object(queryset=qs)
class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView):
"""
Detail view of a single object on a single date; this differs from the
standard DetailView by accepting a year/month/day in the URL.
"""
template_name_suffix = '_detail'
def _date_from_string(year, year_format, month='', month_format='', day='', day_format='', delim='__'):
"""
Helper: get a datetime.date object given a format string and a year,
month, and day (only year is mandatory). Raise a 404 for an invalid date.
"""
format = delim.join((year_format, month_format, day_format))
datestr = delim.join((year, month, day))
try:
return datetime.datetime.strptime(force_str(datestr), format).date()
except ValueError:
raise Http404(_("Invalid date string '%(datestr)s' given format '%(format)s'") % {
'datestr': datestr,
'format': format,
})
def _get_next_prev(generic_view, date, is_previous, period):
"""
Helper: Get the next or the previous valid date. The idea is to allow
links on month/day views to never be 404s by never providing a date
that'll be invalid for the given view.
This is a bit complicated since it handles different intervals of time,
hence the coupling to generic_view.
However in essence the logic comes down to:
* If allow_empty and allow_future are both true, this is easy: just
return the naive result (just the next/previous day/week/month,
regardless of object existence.)
* If allow_empty is true, allow_future is false, and the naive result
isn't in the future, then return it; otherwise return None.
* If allow_empty is false and allow_future is true, return the next
date *that contains a valid object*, even if it's in the future. If
there are no next objects, return None.
* If allow_empty is false and allow_future is false, return the next
date that contains a valid object. If that date is in the future, or
if there are no next objects, return None.
"""
date_field = generic_view.get_date_field()
allow_empty = generic_view.get_allow_empty()
allow_future = generic_view.get_allow_future()
get_current = getattr(generic_view, '_get_current_%s' % period)
get_next = getattr(generic_view, '_get_next_%s' % period)
# Bounds of the current interval
start, end = get_current(date), get_next(date)
# If allow_empty is True, the naive result will be valid
if allow_empty:
if is_previous:
result = get_current(start - datetime.timedelta(days=1))
else:
result = end
if allow_future or result <= effective_datetime.today():
return result
else:
return None
# Otherwise, we'll need to go to the database to look for an object
# whose date_field is at least (greater than/less than) the given
# naive result
else:
# Construct a lookup and an ordering depending on whether we're doing
# a previous date or a next date lookup.
if is_previous:
lookup = {'%s__lt' % date_field: generic_view._make_date_lookup_arg(start)}
ordering = '-%s' % date_field
else:
lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg(end)}
ordering = date_field
# Filter out objects in the future if appropriate.
if not allow_future:
# Fortunately, to match the implementation of allow_future,
# we need __lte, which doesn't conflict with __lt above.
if generic_view.uses_datetime_field:
now = effective_datetime.now()
else:
now = effective_datetime.today()
lookup['%s__lte' % date_field] = now
qs = generic_view.get_queryset().filter(**lookup).order_by(ordering)
# Snag the first object from the queryset; if it doesn't exist that
# means there's no next/previous link available.
try:
result = getattr(qs[0], date_field)
except IndexError:
return None
# Convert datetimes to dates in the current time zone.
if generic_view.uses_datetime_field:
if settings.USE_TZ:
result = effective_datetime.localtime(result)
result = result.date()
# Return the first day of the period.
return get_current(result)
Loading…
Cancel
Save