13 changed files with 773 additions and 0 deletions
			
			
		| @ -0,0 +1,9 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | from django.utils.translation import ugettext_lazy as _ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | SLUG_HELP = _("Kurzfassung des Namens für die Adresszeile im Browser. Vorzugsweise englisch, keine Umlaute, nur Bindestrich als Sonderzeichen.") | ||||||
|  | 
 | ||||||
| @ -0,0 +1,224 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | """ | ||||||
|  | Extends django.utils.dateformat | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import datetime | ||||||
|  | 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_text | ||||||
|  | from django.utils.translation import get_language, ugettext_lazy as _ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Adding "q" | ||||||
|  | re_formatchars = re.compile(r'(?<!\\)([aAbBcdDeEfFgGhHiIjlLmMnNoOPqrsStTUuwWyYzZ])') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ExtendedFormat(DateFormat): | ||||||
|  |     def q(self): | ||||||
|  |         """ | ||||||
|  |         Time, in 24-hour hours and minutes, with minutes left off if they're | ||||||
|  |         zero. | ||||||
|  |         Examples: '1', '1:30', '13:05', '14' | ||||||
|  |         Proprietary extension. | ||||||
|  |         """ | ||||||
|  |         if self.data.minute == 0: | ||||||
|  |             return self.G() | ||||||
|  |         return '%s:%s' % (self.G(), self.i()) | ||||||
|  | 
 | ||||||
|  |     def format(self, formatstr): | ||||||
|  |         pieces = [] | ||||||
|  |         for i, piece in enumerate(re_formatchars.split(force_text(formatstr))): | ||||||
|  |             if i % 2: | ||||||
|  |                 pieces.append(force_text(getattr(self, piece)())) | ||||||
|  |             elif piece: | ||||||
|  |                 pieces.append(re_escaped.sub(r'\1', piece)) | ||||||
|  |         return ''.join(pieces) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def format(value, format): | ||||||
|  |     # Copy of django.utils.dateformat.format, using our extended formatter | ||||||
|  |     df = ExtendedFormat(value) | ||||||
|  |     return df.format(format) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def time_format(value, format=None, use_l10n=None): | ||||||
|  |     # Copy of django.utils.dateformat.time_format, using our extended formatter | ||||||
|  |     tf = ExtendedFormat(value) | ||||||
|  |     return tf.format(get_format(format or 'DATE_FORMAT', use_l10n=use_l10n)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _normalize_variant(variant): | ||||||
|  |     if variant.lower() not in ('short', 'long', ''): | ||||||
|  |         variant = 'short' | ||||||
|  |     if variant and not variant.endswith("_"): | ||||||
|  |         variant = variant + "_" | ||||||
|  |     return variant.upper() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def format_date_range(from_date, to_date, variant='short'): | ||||||
|  |     """ | ||||||
|  |     >>> import datetime | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2009,1,20)) | ||||||
|  |     '15. - 20.01.2009.' | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2009,2,20)) | ||||||
|  |     '15.01. - 20.02.2009.' | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2010,2,20)) | ||||||
|  |     '15.01.2009. - 20.02.2010.' | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2010,1,20)) | ||||||
|  |     '15.01.2009. - 20.01.2010.' | ||||||
|  |     """ | ||||||
|  |     if not (from_date or to_date): | ||||||
|  |         return "" | ||||||
|  | 
 | ||||||
|  |     variant = _normalize_variant(variant) | ||||||
|  | 
 | ||||||
|  |     # Only deal with dates, ignoring time | ||||||
|  |     def datetime_to_date(dt): | ||||||
|  |         try: | ||||||
|  |             return dt.date() | ||||||
|  |         except AttributeError: | ||||||
|  |             return dt | ||||||
|  |     from_date = datetime_to_date(from_date) | ||||||
|  |     to_date = datetime_to_date(to_date) | ||||||
|  | 
 | ||||||
|  |     from_format = to_format = get_format(variant + 'DATE_FORMAT') | ||||||
|  | 
 | ||||||
|  |     if from_date == to_date or not to_date: | ||||||
|  |         return date_format(from_date, get_format(from_format)) | ||||||
|  |     else: | ||||||
|  |         if (from_date.year == to_date.year): | ||||||
|  |             from_format = get_format(variant + 'DAYMONTH_FORMAT') or 'd/m/' | ||||||
|  |             if (from_date.month == to_date.month): | ||||||
|  |                 from_format = get_format(variant + 'DAYONLY_FORMAT') or 'd' | ||||||
|  | 
 | ||||||
|  |         f = t = "" | ||||||
|  |         if from_date: | ||||||
|  |             f = date_format(from_date, get_format(from_format)) | ||||||
|  |         if to_date: | ||||||
|  |             t = date_format(to_date, get_format(to_format)) | ||||||
|  | 
 | ||||||
|  |         separator = get_format('DATE_RANGE_SEPARATOR') or " - " | ||||||
|  |         return separator.join((f, t)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def format_time_range(from_time, to_time, variant='short'): | ||||||
|  |     """ | ||||||
|  |     Knows how to deal with left out from_time/to_time values. | ||||||
|  |     """ | ||||||
|  |     if not (from_time or to_time): | ||||||
|  |         return "" | ||||||
|  | 
 | ||||||
|  |     variant = _normalize_variant(variant) | ||||||
|  | 
 | ||||||
|  |     from_format = to_format = "q"  # get_format(variant + 'TIME_FORMAT') | ||||||
|  | 
 | ||||||
|  |     if from_time == to_time or not to_time: | ||||||
|  |         return time_format(from_time, get_format(from_format)) | ||||||
|  |     else: | ||||||
|  |         f = t = "" | ||||||
|  |         if from_time: | ||||||
|  |             f = time_format(from_time, get_format(from_format)) | ||||||
|  |         if to_time: | ||||||
|  |             t = time_format(to_time, get_format(to_format)) | ||||||
|  | 
 | ||||||
|  |         separator = get_format('DATE_RANGE_SEPARATOR') or "–" | ||||||
|  |         return separator.join((f, t)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def format_timespan_range(timespan_object, force_wholeday=False, variant='short'): | ||||||
|  |     """ | ||||||
|  |     For Timespan-objects, i.e. object with start_date, end_date, start_time and end_time properties. | ||||||
|  | 
 | ||||||
|  |     Multiday or force_wholeday: | ||||||
|  |     "10.07.2016-11.07.2016" | ||||||
|  | 
 | ||||||
|  |     Single days: | ||||||
|  |     "10.07.2016 11 Uhr" | ||||||
|  |     "10.07.2016 11-14 Uhr" | ||||||
|  | 
 | ||||||
|  |     >>> import datetime | ||||||
|  |     >>> sd, ed = datetime.date(2009,1,15), datetime.date(2009,1,20) | ||||||
|  |     >>> st, et = datetime.date(2009,1,15), datetime.date(2009,1,20) | ||||||
|  |     >>> class TestObject(object): | ||||||
|  |     >>>     start_date = None | ||||||
|  |     >>>     end_date = None | ||||||
|  |     >>>     start_time = None | ||||||
|  |     >>>     end_time = None | ||||||
|  |     >>> obj = TestObject() | ||||||
|  |     >>> obj.start_date = obj.end_date = sd | ||||||
|  |     >>> format_timespan_range(obj) | ||||||
|  |     '15.01.–20.01.2009' | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     variant = _normalize_variant(variant) | ||||||
|  | 
 | ||||||
|  |     rv = format_date_range(timespan_object.start_date, timespan_object.end_date, variant) | ||||||
|  | 
 | ||||||
|  |     if (timespan_object.is_multiday() or | ||||||
|  |        not timespan_object.start_time or | ||||||
|  |        force_wholeday): | ||||||
|  |         # Don't show timespan | ||||||
|  |         return rv | ||||||
|  |     else: | ||||||
|  |         rv = _("%(daterange)s %(timespan)s Uhr") % { | ||||||
|  |             'daterange': rv, | ||||||
|  |             'timespan': format_time_range(timespan_object.start_time, timespan_object.end_time, variant) | ||||||
|  |         } | ||||||
|  |     return rv | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def format_partial_date(year=None, month=None, day=None, variant='short'): | ||||||
|  |     """ | ||||||
|  |     >>> format_partial_date(2008) | ||||||
|  |     2008 | ||||||
|  |     >>> format_partial_date(2008, 3) | ||||||
|  |     2008 | ||||||
|  |     >>> format_partial_date(2008) | ||||||
|  |     2008 | ||||||
|  |     >>> format_partial_date(2008) | ||||||
|  |     2008 | ||||||
|  |     >>> format_partial_date(2008) | ||||||
|  |     2008 | ||||||
|  |     >>> format_partial_date(2008) | ||||||
|  |     2008 | ||||||
|  |     """ | ||||||
|  |     if year and month and day: | ||||||
|  |         format_name = 'DATE_FORMAT' | ||||||
|  |     elif year and month: | ||||||
|  |         format_name = 'YEARMONTH_FORMAT' | ||||||
|  |     elif month and day: | ||||||
|  |         format_name = 'DAYMONTH_FORMAT' | ||||||
|  |     elif year: | ||||||
|  |         format_name = 'YEAR_FORMAT' | ||||||
|  |     elif month: | ||||||
|  |         format_name = 'MONTH_FORMAT' | ||||||
|  |     elif day: | ||||||
|  |         format_name = 'DAYONLY_FORMAT' | ||||||
|  | 
 | ||||||
|  |     name = _normalize_variant(variant) + format_name | ||||||
|  |     # TODO Django bug or what? Sometimes get_language returns None, therefore force a language here | ||||||
|  |     partial_date_format = get_format(name, lang=get_language() or settings.LANGUAGE_CODE) | ||||||
|  |     return date_format(datetime.date(year or 2000, month or 1, day or 1), partial_date_format) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO Add format_partial_date_range function | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _test(): | ||||||
|  |     import doctest | ||||||
|  |     doctest.testmod() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     _test() | ||||||
| @ -0,0 +1,79 @@ | |||||||
|  | # -*- 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.template.defaultfilters import slugify | ||||||
|  | from django.utils.translation import ugettext_lazy as _ | ||||||
|  | 
 | ||||||
|  | from . import SLUG_HELP | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | DEFAULT_SLUG = _("item") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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(slug_value) | ||||||
|  |     index = 0 | ||||||
|  |     if not queryset: | ||||||
|  |         queryset = instance.__class__._default_manager.get_queryset() | ||||||
|  | 
 | ||||||
|  |     def get_similar_slugs(slug): | ||||||
|  |         return queryset.exclude(pk=instance.pk) \ | ||||||
|  |             .filter(**{"%s__istartswith" % slug_field: slug}).values_list(slug_field, flat=True) | ||||||
|  | 
 | ||||||
|  |     similar_slugs = get_similar_slugs(slug) | ||||||
|  |     while slug in similar_slugs or len(slug) > max_length: | ||||||
|  |         index += 1 | ||||||
|  |         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 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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 super(AutoSlugField, self).pre_save(model_instance, add) | ||||||
| @ -0,0 +1,51 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | from django import forms | ||||||
|  | from django.contrib import admin | ||||||
|  | from django.utils.translation import ugettext_lazy as _ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # from http://stackoverflow.com/questions/877723/inline-form-validation-in-django#877920 | ||||||
|  | 
 | ||||||
|  | class MandatoryInlineFormSet(forms.models.BaseInlineFormSet): | ||||||
|  |     """ | ||||||
|  |     Make sure at least one inline form is valid. | ||||||
|  |     """ | ||||||
|  |     mandatory_error_message = _("Bitte mindestens %(min_num)s %(name)s angeben.") | ||||||
|  | 
 | ||||||
|  |     def is_valid(self): | ||||||
|  |         return super(MandatoryInlineFormSet, self).is_valid() and \ | ||||||
|  |                      not any([bool(e) for e in self.errors]) | ||||||
|  | 
 | ||||||
|  |     def clean(self): | ||||||
|  |         # get forms that actually have valid data | ||||||
|  |         count = 0 | ||||||
|  |         for form in self.forms: | ||||||
|  |             try: | ||||||
|  |                 if form.cleaned_data and not form.cleaned_data.get('DELETE', False): | ||||||
|  |                     count += 1 | ||||||
|  |             except AttributeError: | ||||||
|  |                 # annoyingly, if a subform is invalid Django explicity raises | ||||||
|  |                 # an AttributeError for cleaned_data | ||||||
|  |                 pass | ||||||
|  |         if count < self.min_num: | ||||||
|  |             if self.min_num > 1: | ||||||
|  |                 name = self.model._meta.verbose_name_plural | ||||||
|  |             else: | ||||||
|  |                 name = self.model._meta.verbose_name | ||||||
|  |             raise forms.ValidationError( | ||||||
|  |                 self.mandatory_error_message % { | ||||||
|  |                     'min_num': self.min_num, | ||||||
|  |                     'name': name, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MandatoryTabularInline(admin.TabularInline): | ||||||
|  |     formset = MandatoryInlineFormSet | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MandatoryStackedInline(admin.StackedInline): | ||||||
|  |     formset = MandatoryInlineFormSet | ||||||
| @ -0,0 +1,67 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | #            improved from https://djangosnippets.org/snippets/1405/ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from django import template | ||||||
|  | from ..dateformat import date_format, get_format | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | # TODO Describe custom formats | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | register = template.Library() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.simple_tag | ||||||
|  | def format_date_range(from_date, to_date, variant='short'): | ||||||
|  |     """ | ||||||
|  |     >>> import datetime | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2009,1,20)) | ||||||
|  |     '15. - 20.01.2009.' | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2009,2,20)) | ||||||
|  |     '15.01. - 20.02.2009.' | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2010,2,20)) | ||||||
|  |     '15.01.2009. - 20.02.2010.' | ||||||
|  |     >>> format_date_range(datetime.date(2009,1,15), datetime.date(2010,1,20)) | ||||||
|  |     '15.01.2009. - 20.01.2010.' | ||||||
|  | 
 | ||||||
|  |     Use in django templates: | ||||||
|  | 
 | ||||||
|  |     {% load date_range %} | ||||||
|  |     {% format_date_range exhibition.start_on exhibition.end_on %} | ||||||
|  |     """ | ||||||
|  |     if variant.lower() not in ('short', 'long', ''): | ||||||
|  |         variant = 'short' | ||||||
|  |     if variant.endswith("_"): | ||||||
|  |         variant = variant + "_" | ||||||
|  | 
 | ||||||
|  |     from_format = to_format = get_format(variant.upper() + 'DATE_FORMAT') | ||||||
|  | 
 | ||||||
|  |     if from_date == to_date: | ||||||
|  |         return date_format(to_date, get_format(to_format)) | ||||||
|  | 
 | ||||||
|  |     if (from_date.year == to_date.year): | ||||||
|  |         from_format = get_format(variant.upper() + 'DAYMONTH_FORMAT') or 'd/m/' | ||||||
|  |         if (from_date.month == to_date.month): | ||||||
|  |             from_format = get_format(variant.upper() + 'DAYONLY_FORMAT') or 'd' | ||||||
|  |     separator = get_format('DATE_RANGE_SEPARATOR') or "–" | ||||||
|  |     # import ipdb; ipdb.set_trace() | ||||||
|  | 
 | ||||||
|  |     print from_format, to_format | ||||||
|  | 
 | ||||||
|  |     f = date_format(from_date, get_format(from_format)) | ||||||
|  |     t = date_format(to_date, get_format(to_format)) | ||||||
|  | 
 | ||||||
|  |     return variant.upper() + " " + separator.join((f, t)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _test(): | ||||||
|  |     import doctest | ||||||
|  |     doctest.testmod() | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     _test() | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | from django import template | ||||||
|  | from django.template.defaultfilters import stringfilter | ||||||
|  | from django.utils.html import conditional_escape | ||||||
|  | from django.utils.safestring import mark_safe | ||||||
|  | from .. import markdown_utils | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | register = template.Library() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.filter(needs_autoescape=True) | ||||||
|  | @stringfilter | ||||||
|  | def inline_markdown(text, autoescape=None, **kwargs): | ||||||
|  |     """ Doesn't wrap the markup in a HTML paragraph. """ | ||||||
|  |     if autoescape: | ||||||
|  |         esc = conditional_escape | ||||||
|  |     else: | ||||||
|  |         esc = lambda x: x | ||||||
|  |     return mark_safe(markdown_utils.inline_markdown_processor.convert(esc(text), **kwargs)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.filter(needs_autoescape=True) | ||||||
|  | @stringfilter | ||||||
|  | def markdown(text, autoescape=None, **kwargs): | ||||||
|  |     if autoescape: | ||||||
|  |         esc = conditional_escape | ||||||
|  |     else: | ||||||
|  |         esc = lambda x: x | ||||||
|  |     return mark_safe(markdown_utils.markdown_processor.convert(esc(text), **kwargs)) | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | from django import template | ||||||
|  | from django.template.defaultfilters import stringfilter | ||||||
|  | from django.utils.html import conditional_escape | ||||||
|  | from django.utils.safestring import mark_safe | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | register = template.Library() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.filter() | ||||||
|  | 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 = value.strip() | ||||||
|  |     if value: | ||||||
|  |         if value[-1] not in ".,:;!?": | ||||||
|  |             value += conditional_escape(punctuation) | ||||||
|  |         value += conditional_escape(space)  # Append previously stripped space | ||||||
|  |     return value | ||||||
|  | conditional_punctuation.is_safe = True | ||||||
| @ -0,0 +1,24 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2014-2015 | ||||||
|  | 
 | ||||||
|  | from django import template | ||||||
|  | from django.db import models | ||||||
|  | 
 | ||||||
|  | from ..translation import get_translation, get_translated_field | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | register = template.Library() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.filter | ||||||
|  | def translation(obj): | ||||||
|  |     return get_translation(obj) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.filter | ||||||
|  | def translate(obj, field_name): | ||||||
|  |     return get_translated_field(obj, field_name) | ||||||
|  | 
 | ||||||
|  | # Alias | ||||||
|  | translated_field = translate | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | from django.utils.text import slugify | ||||||
|  | from django.utils import six | ||||||
|  | from django.utils.encoding import force_text | ||||||
|  | from django.utils.functional import allow_lazy | ||||||
|  | from django.utils.safestring import SafeText | ||||||
|  | 
 | ||||||
|  | # import unicodedata | ||||||
|  | import translitcodec | ||||||
|  | import codecs | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def downgrade(value): | ||||||
|  |     """ | ||||||
|  |     Downgrade unicode to ascii, transliterating accented characters. | ||||||
|  |     """ | ||||||
|  |     value = force_text(value) | ||||||
|  |     return codecs.encode(value, 'translit/long') | ||||||
|  | downgrade = allow_lazy(downgrade, six.text_type, SafeText) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def slugify_long(value): | ||||||
|  |     value = force_text(value) | ||||||
|  |     return slugify(downgrade(value)) | ||||||
|  | slugify_long = allow_lazy(slugify_long, six.text_type, SafeText) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def slugify_german(value): | ||||||
|  |     """ | ||||||
|  |     Transliterates Umlaute before calling django's slugify function. | ||||||
|  |     """ | ||||||
|  |     umlaute = { | ||||||
|  |         'Ä': 'Ae', | ||||||
|  |         'Ö': 'Oe', | ||||||
|  |         'Ü': 'Ue', | ||||||
|  |         'ä': 'ae', | ||||||
|  |         'ö': 'oe', | ||||||
|  |         'ü': 'ue', | ||||||
|  |         'ß': 'ss', | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     value = force_text(value) | ||||||
|  |     umap = {ord(key): unicode(val) for key, val in umlaute.items()} | ||||||
|  |     return slugify(value.translate(umap)) | ||||||
|  | slugify_german = allow_lazy(slugify_german, six.text_type, SafeText) | ||||||
|  | 
 | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | from django.utils import timezone | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def smart_default_tz(datetime_value): | ||||||
|  |     """ | ||||||
|  |     Returns the give datetime with the default timezone applied. | ||||||
|  |     """ | ||||||
|  |     if timezone.is_naive(datetime_value): | ||||||
|  |         datetime_value = timezone.make_aware(datetime_value, timezone=timezone.get_default_timezone()) | ||||||
|  |     return timezone.localtime(datetime_value, timezone.get_default_timezone()) | ||||||
|  | 
 | ||||||
| @ -0,0 +1,195 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | 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.template.loader import select_template | ||||||
|  | from django.utils import translation | ||||||
|  | from django.views.generic import TemplateView | ||||||
|  | from django.utils.translation import check_for_language, LANGUAGE_SESSION_KEY | ||||||
|  | from django.utils.http import is_safe_url | ||||||
|  | from django.http import HttpResponseRedirect | ||||||
|  | from django.views.i18n import LANGUAGE_QUERY_PARAMETER | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def lang_suffix(language_code=None): | ||||||
|  |     """ | ||||||
|  |     Returns the suffix appropriate for adding to field names for selecting | ||||||
|  |     the current language. | ||||||
|  |     """ | ||||||
|  |     if not language_code: | ||||||
|  |         language_code = translation.get_language() | ||||||
|  |     if not language_code: | ||||||
|  |         language_code = settings.LANGUAGE_CODE | ||||||
|  |     language_code = language_code[:2] or 'de'  # FIXME Fall back to default language | ||||||
|  |     return "_%s" % language_code | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DirectTemplateView(TemplateView): | ||||||
|  |     extra_context = None | ||||||
|  | 
 | ||||||
|  |     def get_context_data(self, **kwargs): | ||||||
|  |         context = super(DirectTemplateView, self).get_context_data(**kwargs) | ||||||
|  |         if self.extra_context is not None: | ||||||
|  |             for key, value in self.extra_context.items(): | ||||||
|  |                 if callable(value): | ||||||
|  |                     context[key] = value() | ||||||
|  |                 else: | ||||||
|  |                     context[key] = value | ||||||
|  |         return context | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class I18nDirectTemplateView(DirectTemplateView): | ||||||
|  |     def get_template_names(self): | ||||||
|  |         t_name, t_ext = os.path.splitext(self.template_name) | ||||||
|  |         lang = translation.get_language() | ||||||
|  |         template_name = select_template(( | ||||||
|  |             "%s.%s%s" % (t_name, lang, t_ext), | ||||||
|  |             self.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 = language_code or translation.get_language()[:2] | ||||||
|  |     try: | ||||||
|  |         return getattr(obj, relation_name).get(language=language_code) | ||||||
|  |     except ObjectDoesNotExist: | ||||||
|  |         try: | ||||||
|  |             return getattr(obj, relation_name).get(language=(language_code == 'en' and 'de' or 'en')) | ||||||
|  |         except ObjectDoesNotExist: | ||||||
|  |             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 | ||||||
|  |     selected language by appending "_<language_code>" to the attribute | ||||||
|  |     name and returning the value. | ||||||
|  | 
 | ||||||
|  |     On AttributeError try to return the other language or the attribute | ||||||
|  |     without the language suffix. | ||||||
|  | 
 | ||||||
|  |     If the attribute is empty or null, try to return the value of | ||||||
|  |     the other language's attribute. | ||||||
|  | 
 | ||||||
|  |     If there is an attribute with the name without any language code | ||||||
|  |     extension, return the value of this. | ||||||
|  | 
 | ||||||
|  |     Best return value: | ||||||
|  |         field_name + lang_suffix for current language | ||||||
|  | 
 | ||||||
|  |     If empty or field does not exist: | ||||||
|  |         if default language and field_name | ||||||
|  |             field_name | ||||||
|  |         else | ||||||
|  |             field_name + lang_suffix other language | ||||||
|  |     """ | ||||||
|  |     # TODO Implement multiple languages | ||||||
|  |     language_code = (language_code or | ||||||
|  |             translation.get_language() or | ||||||
|  |             settings.LANGUAGE_CODE)[:2] | ||||||
|  |     is_default_language = bool(language_code == settings.LANGUAGE_CODE[:2]) | ||||||
|  |     if language_code == 'de': | ||||||
|  |         other_language_code = 'en' | ||||||
|  |     else: | ||||||
|  |         other_language_code = 'de' | ||||||
|  | 
 | ||||||
|  |     def has_db_field(field_name): | ||||||
|  |         try: | ||||||
|  |             # Only try to access database fields to avoid recursion | ||||||
|  |             obj._meta.get_field(field_name) | ||||||
|  |             return True | ||||||
|  |         except FieldDoesNotExist: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     translated_field_name = '%s_%s' % (field_name, language_code) | ||||||
|  |     other_translated_field_name = '%s_%s' % (field_name, other_language_code) | ||||||
|  |     rv = "" | ||||||
|  |     if hasattr(obj, translated_field_name): | ||||||
|  |         rv = getattr(obj, translated_field_name) | ||||||
|  |     if not rv: | ||||||
|  |         if is_default_language and has_db_field(field_name): | ||||||
|  |             rv = getattr(obj, field_name) | ||||||
|  |         elif hasattr(obj, other_translated_field_name): | ||||||
|  |             rv = getattr(obj, other_translated_field_name) | ||||||
|  |     if not rv and has_db_field(field_name): | ||||||
|  |         rv = getattr(obj, field_name) | ||||||
|  |     # FIXME Raise error if neither field exists | ||||||
|  |     return rv | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @contextmanager | ||||||
|  | def active_language(lang='de'): | ||||||
|  |     translation.activate(lang) | ||||||
|  |     yield | ||||||
|  |     translation.deactivate() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def set_language(request): | ||||||
|  |     """ | ||||||
|  |     Modified copy from django.views.i18n | ||||||
|  |     """ | ||||||
|  |     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 | ||||||
					Loading…
					
					
				
		Reference in new issue