# -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime import difflib import json import logging import os import re import unicodedata from babel.dates import format_date from django.conf import settings from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.db import models from django.template.loader import get_template from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import get_language, ugettext_lazy as _ from django_countries.fields import Country, CountryField from .. import choices from .. import config from .. import signals from ..workflow import DefaultWorkflow from . import get_ghost_user from .eventchange import EventChange logger = logging.getLogger(__name__) @python_2_unicode_compatible class Event(models.Model): # Metadata owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET(get_ghost_user), related_name='events', verbose_name=_('Ersteller')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Erstellt')) number = models.CharField(unique=True, max_length=config.NUMBER_MAX_LENGTH, blank=True, null=True, default=None, verbose_name=_('Programmnummer')) # DescriptionForm title = models.CharField(max_length=config.TITLE_MAX_LENGTH, verbose_name=_('Titel')) description = models.TextField(verbose_name=_('Beschreibung')) # ModeForm mode = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.MODE_CHOICES, verbose_name=_('Veranstaltungsart')) sport = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.SPORT_CHOICES, verbose_name=_('Spielart')) ski_lift = models.BooleanField(default=False, verbose_name=_('Skiliftbenutzung')) level = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.LEVEL_CHOICES, verbose_name=_('Schwierigkeitsnivau')) first_day = models.DateField(verbose_name=_('Erster Tag')) alt_first_day = models.DateField(blank=True, null=True, verbose_name='%s - %s' % (_('Ersatztermin'), _('Erster Tag'))) last_day = models.DateField(blank=True, null=True, verbose_name=_('Letzter Tag')) alt_last_day = models.DateField(blank=True, null=True, verbose_name='%s - %s' % (_('Ersatztermin'), _('Letzter Tag'))) # LocationForm country = CountryField(countries=choices.CountryChoiceSet, verbose_name=_('Land')) terrain = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.TERRAIN_CHOICES, default=choices.TERRAIN_CHOICES[0][0], verbose_name=_('Gelände')) location = models.CharField(max_length=config.LOCATION_MAX_LENGTH, blank=True, verbose_name=_('Ort oder Gebiet')) transport = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.TRANSPORT_CHOICES, default=choices.TRANSPORT_CHOICES[0][0], verbose_name=_('Verkehrsmittel')) transport_other = models.CharField(max_length=config.TRANSPORT_OTHER_MAX_LENGTH, blank=True, verbose_name=_('Anderes Verkehrsmittel')) # JourneyForm meeting_point = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.MEETING_POINT_CHOICES, default=choices.MEETING_POINT_CHOICES[0][0], verbose_name=_('Treffpunkt')) meeting_point_other = models.CharField(max_length=config.MEETING_POINT_OTHER_MAX_LENGTH, blank=True, verbose_name=_('Anderer Treffpunkt')) meeting_time = models.TimeField(blank=True, null=True, verbose_name=_('Uhrzeit am Treffpunkt')) departure_time = models.TimeField(blank=True, null=True, verbose_name=_('Uhrzeit Abfahrt')) departure_ride = models.CharField(max_length=config.DEPARTURE_RIDE_MAX_LENGTH, blank=True, verbose_name=_('Bahn-/Bus-Linie')) return_departure_time = models.TimeField(blank=True, null=True, verbose_name=_('Uhrzeit Rückfahrt')) return_arrival_time = models.TimeField(blank=True, null=True, verbose_name=_('Uhrzeit Rückkunft')) arrival_previous_day = models.BooleanField(default=False, verbose_name=_('Anreise des Trainers am Vortag')) # AccommodationForm basecamp = models.CharField(max_length=config.BASECAMP_MAX_LENGTH, blank=True, verbose_name=_('Stützpunkt')) accommodation = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.ACCOMMODATION_CHOICES, default=choices.ACCOMMODATION_CHOICES[0][0], verbose_name=_('Unterkunft')) accommodation_other = models.CharField(max_length=config.ACCOMMODATION_OTHER_MAX_LENGTH, blank=True, verbose_name=_('Andere Unterkunft')) meals = models.CharField(max_length=choices.CHOICE_FIELD_MAX_LENGTH, choices=choices.MEALS_CHOICES, default=choices.MEALS_CHOICES[0][0], verbose_name=_('Verpflegung')) meals_other = models.CharField(max_length=config.MEALS_OTHER_MAX_LENGTH, blank=True, verbose_name=_('Andere Verpflegung')) # RequirementsForm requirements = models.TextField(blank=True, verbose_name=_('Anforderungen')) equipment = models.TextField(blank=True, verbose_name=_('Ausrüstung')) pre_meeting_1 = models.DateTimeField(blank=True, null=True, verbose_name='1. %s' % _('Vortreffen')) pre_meeting_2 = models.DateTimeField(blank=True, null=True, verbose_name='2. %s' % _('Vortreffen')) # TrainerForm trainer_firstname = models.CharField(max_length=config.TRAINER_NAME_MAX_LENGTH, blank=True, verbose_name='1. %s %s' % (_('Trainer'), _('Vorname'))) trainer_familyname = models.CharField(max_length=config.TRAINER_NAME_MAX_LENGTH, blank=True, verbose_name='1. %s %s' % (_('Trainer'), _('Familienname'))) trainer_email = models.EmailField(blank=True, verbose_name='1. %s %s' % (_('Trainer'), _('E-Mail'))) trainer_phone = models.CharField(max_length=config.PHONE_NUMBER_MAX_LENGTH, blank=True, verbose_name='1. %s %s' % (_('Trainer'), _('Telefon'))) trainer_2_fullname = models.CharField(max_length=config.TRAINER_NAME_MAX_LENGTH, blank=True, verbose_name='2. %s %s' % (_('Trainer'), _('Name'))) trainer_2_email = models.EmailField(blank=True, verbose_name='2. %s %s' % (_('Trainer'), _('E-Mail'))) trainer_2_phone = models.CharField(max_length=config.PHONE_NUMBER_MAX_LENGTH, blank=True, verbose_name='2. %s %s' % (_('Trainer'), _('Telefon'))) trainer_3_fullname = models.CharField(max_length=config.TRAINER_NAME_MAX_LENGTH, blank=True, verbose_name='3. %s %s' % (_('Trainer'), _('Name'))) trainer_3_email = models.EmailField(blank=True, verbose_name='3. %s %s' % (_('Trainer'), _('E-Mail'))) trainer_3_phone = models.CharField(max_length=config.PHONE_NUMBER_MAX_LENGTH, blank=True, verbose_name='3. %s %s' % (_('Trainer'), _('Telefon'))) # RegistrationForm min_participants = models.IntegerField(default=0, verbose_name=_('Min. Teilnehmer')) max_participants = models.IntegerField(default=0, verbose_name=_('Max. Teilnehmer')) registration_required = models.BooleanField(default=False, verbose_name=_('Anmeldung notwendig')) deadline = models.DateField(blank=True, null=True, verbose_name=_('Anmeldeschluss')) registration_howto = models.TextField(blank=True, verbose_name=_('Anmeldungshinweis')) # ChargesForm charge = models.FloatField(default=0, verbose_name=_('Teilnahmegebühr')) additional_costs = models.CharField(max_length=config.ADDITIONAL_COSTS_MAX_LENGTH, blank=True, verbose_name=_('Zusätzliche Kosten')) # TrainingForm course_topic_1 = models.TextField(blank=True, verbose_name='%s - %s 1' % (_('Kursinhalt'), _('Absatz'))) course_topic_2 = models.TextField(blank=True, verbose_name='%s - %s 2' % (_('Kursinhalt'), _('Absatz'))) course_topic_3 = models.TextField(blank=True, verbose_name='%s - %s 3' % (_('Kursinhalt'), _('Absatz'))) course_topic_4 = models.TextField(blank=True, verbose_name='%s - %s 4' % (_('Kursinhalt'), _('Absatz'))) course_topic_5 = models.TextField(blank=True, verbose_name='%s - %s 5' % (_('Kursinhalt'), _('Absatz'))) course_topic_6 = models.TextField(blank=True, verbose_name='%s - %s 6' % (_('Kursinhalt'), _('Absatz'))) course_goal_1 = models.TextField(blank=True, verbose_name='%s - %s 1' % (_('Kursziele'), _('Absatz'))) course_goal_2 = models.TextField(blank=True, verbose_name='%s - %s 2' % (_('Kursziele'), _('Absatz'))) course_goal_3 = models.TextField(blank=True, verbose_name='%s - %s 3' % (_('Kursziele'), _('Absatz'))) course_goal_4 = models.TextField(blank=True, verbose_name='%s - %s 4' % (_('Kursziele'), _('Absatz'))) course_goal_5 = models.TextField(blank=True, verbose_name='%s - %s 5' % (_('Kursziele'), _('Absatz'))) course_goal_6 = models.TextField(blank=True, verbose_name='%s - %s 6' % (_('Kursziele'), _('Absatz'))) # SummaryForm planned_publication_date = models.DateField(blank=True, null=True, verbose_name=_('Veröffentlichung am')) internal_note = models.TextField(blank=True, verbose_name=_('Bearbeitungshinweis')) registration_closed = models.BooleanField(default=False, verbose_name=_('Anmeldung geschlossen')) @property def workflow(self): return DefaultWorkflow(self) @property def editor(self): if not hasattr(self, '_editor'): self._editor = None return self._editor @editor.setter def editor(self, editor): self._editor = editor class Meta: verbose_name = _('Veranstaltung') verbose_name_plural = _('Veranstaltungen') ordering = ['first_day'] def __str__(self): return '{number} - {title} ({date})'.format(number=self.get_number(), title=self.title, date=self.get_formated_date()) def get_absolute_url(self): return reverse('dav_events:detail', kwargs={'pk': self.pk}) def save(self, implicit_update=False, **kwargs): creating = False original_text = '' if not self.id: user_model = get_user_model() username = self.trainer_email.lower() if not username: s = self.trainer_firstname.replace('ß', 'ss') s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') s = re.sub(r'[^a-z-]', '', s.lower()) username = s s = self.trainer_familyname.replace('ß', 'ss') s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') s = re.sub(r'[^a-z-]', '', s.lower()) username += '.' + s s = re.sub(r'[^0-9]', '', str(self.trainer_phone)) username += '@' + s try: owner = user_model.objects.get(username=username) except user_model.DoesNotExist: owner = user_model(username=username, first_name=self.trainer_firstname, last_name=self.trainer_familyname, email=self.trainer_email, ) owner.save() logger.info('Owner created: %s', owner.username) if self.editor and self.editor.is_authenticated and self.editor != owner: logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner) self.owner = owner creating = True else: original = Event.objects.get(id=self.id) if not self.editor or not self.editor.is_authenticated: self.editor = self.owner super(Event, self).save(**kwargs) if creating: logger.info('Event created: %s', self) signals.event_created.send(sender=self.__class__, event=self) self.workflow.update_status('draft', self.editor) else: change = EventChange(event=self, user=self.editor, operation=EventChange.UPDATE, content=self.diff(original)) change.save() if not implicit_update: logger.info('Event updated: %s', self) signals.event_updated.send(sender=self.__class__, event=self, user=self.editor, diff=self.diff(original, fmt='human_readable')) def diff(self, event, fmt='json'): if fmt == 'human_readable': from_text = event.render_as_text(show_internal_fields=True) to_text = self.render_as_text(show_internal_fields=True) from_lines = from_text.split('\n') to_lines = to_text.split('\n') diff_lines = list(difflib.unified_diff(from_lines, to_lines, n=len(from_lines), lineterm='')) diff_text = '\n'.join(diff_lines[3:]) elif fmt == 'json': fields = self._meta.get_fields() changes = [] for field in fields: field_name = field.name from_value = getattr(event, field_name) try: json.dumps(from_value) except TypeError: from_value = str(from_value) to_value = getattr(self, field_name) try: json.dumps(to_value) except TypeError: to_value = str(to_value) if from_value != to_value: change = { 'field': field_name, 'refer': from_value, 'current': to_value, } changes.append(change) diff_text = json.dumps(changes) else: raise ValueError("Event.diff(): Unsupported format: {}".format(fmt)) return diff_text def is_deadline_expired(self): today = datetime.date.today() return self.deadline and self.deadline < today def get_number(self): return self.workflow.get_number() def get_formated_date(self, begin_date=None, end_date=None, format='normalized_long'): if begin_date is None: begin_date = self.first_day if end_date is None: end_date = self.last_day if format.endswith('numeric'): weekday_fmt = '' day_fmt = 'dd.' month_fmt = 'MM.' year_fmt = 'yyyy' elif format.endswith('short'): weekday_fmt = 'EEE ' day_fmt = 'd.' month_fmt = ' MMM' if begin_date.year == datetime.date.today().year: year_fmt = '' else: year_fmt = ' yy' else: weekday_fmt = 'EEEE, ' day_fmt = 'dd.' month_fmt = ' MMMM' year_fmt = ' yyyy' lang = get_language()[0:2] if not end_date: fmt = '{weekday}{day}{month}{year}'.format(weekday=weekday_fmt, day=day_fmt, month=month_fmt, year=year_fmt) r = format_date(begin_date, fmt, locale=lang) else: end_format = '{weekday}{day}{month}{year}'.format(weekday=weekday_fmt, day=day_fmt, month=month_fmt, year=year_fmt) if format.startswith('normalized'): begin_format = '{weekday}{day}'.format(weekday=weekday_fmt, day=day_fmt) if begin_date.month != end_date.month: begin_format += month_fmt if begin_date.year != end_date.year: begin_format += year_fmt else: begin_format = '{weekday}{day}{month}{year}'.format(weekday=weekday_fmt, day=day_fmt, month=month_fmt, year=year_fmt) begin = format_date(begin_date, begin_format, locale=lang) end = format_date(end_date, end_format, locale=lang) r = '{begin} - {end}'.format(begin=begin, end=end) return r def get_alt_formated_date(self, format='normalized_long'): if self.alt_first_day: return self.get_formated_date(begin_date=self.alt_first_day, end_date=self.alt_last_day, format=format) else: return None def get_numeric_date(self, begin_date=None, end_date=None): return self.get_formated_date(begin_date=begin_date, end_date=end_date, format='numeric') def get_trainer_full_name(self): return '%s %s' % (self.trainer_firstname, self.trainer_familyname) def get_template_context(self, context=None): if self.alt_last_day: day_after = self.alt_last_day + datetime.timedelta(1) elif self.last_day: day_after = self.last_day + datetime.timedelta(1) elif self.alt_first_day: day_after = self.alt_first_day + datetime.timedelta(1) else: day_after = self.first_day + datetime.timedelta(1) r = { 'event': self, 'number': self.get_number(), 'title': self.title, 'description': self.description, 'mode': self.mode, 'sport': self.sport, 'first_day': self.first_day, 'last_day': self.last_day, 'normalized_date': self.get_formated_date(format='normalized'), 'normalized_long_date': self.get_formated_date(format='normalized_long'), 'normalized_short_date': self.get_formated_date(format='normalized_short'), 'alt_first_day': self.alt_first_day, 'alt_last_day': self.alt_last_day, 'alt_normalized_date': self.get_alt_formated_date(format='normalized'), 'alt_normalized_long_date': self.get_alt_formated_date(format='normalized_long'), 'alt_normalized_short_date': self.get_alt_formated_date(format='normalized_short'), 'day_after': day_after, 'country': self.country, 'location': self.location, 'transport': self.transport, 'transport_other': self.transport_other, 'meeting_point': self.meeting_point, 'meeting_point_other': self.meeting_point_other, 'meeting_time': self.meeting_time, 'departure_time': self.departure_time, 'departure_ride': self.departure_ride, 'return_departure_time': self.return_departure_time, 'return_arrival_time': self.return_arrival_time, 'basecamp': self.basecamp, 'accommodation': self.accommodation, 'accommodation_other': self.accommodation_other, 'meals': self.meals, 'meals_other': self.meals_other, 'requirements': self.requirements, 'equipment': self.equipment, 'pre_meeting_1': self.pre_meeting_1, 'pre_meeting_2': self.pre_meeting_2, 'trainer_firstname': self.trainer_firstname, 'trainer_familyname': self.trainer_familyname, 'trainer_fullname': self.get_trainer_full_name(), 'trainer_email': self.trainer_email, 'trainer_phone': self.trainer_phone, 'trainer_2_fullname': self.trainer_2_fullname, 'trainer_2_email': self.trainer_2_email, 'trainer_2_phone': self.trainer_2_phone, 'trainer_3_fullname': self.trainer_3_fullname, 'trainer_3_email': self.trainer_3_email, 'trainer_3_phone': self.trainer_3_phone, 'min_participants': self.min_participants, 'max_participants': self.max_participants, 'registration_required': self.registration_required, 'deadline': self.deadline, 'registration_howto': self.registration_howto, 'charge': self.charge, 'additional_costs': self.additional_costs, 'course_topic_1': self.course_topic_1, 'course_topic_2': self.course_topic_2, 'course_topic_3': self.course_topic_3, 'course_topic_4': self.course_topic_4, 'course_topic_5': self.course_topic_5, 'course_topic_6': self.course_topic_6, 'course_goal_1': self.course_goal_1, 'course_goal_2': self.course_goal_2, 'course_goal_3': self.course_goal_3, 'course_goal_4': self.course_goal_4, 'course_goal_5': self.course_goal_5, 'course_goal_6': self.course_goal_6, 'planned_publication_date': self.planned_publication_date, 'internal_note': self.internal_note, 'registration_closed': self.registration_closed, } if context is not None: r.update(context) return r def render_as_text(self, format=None, show_internal_fields=False): if format == 'ka-alpin': template_name = 'ka-alpin.txt' else: template_name = 'default.txt' template_path = os.path.join('dav_events', 'event', template_name) template = get_template(template_path, using='PLAINTEXT') return template.render(self.get_template_context({'show_internal_fields': show_internal_fields})) def render_as_html(self): template_name = os.path.join('dav_events', 'event', 'default.html') template = get_template(template_name) return template.render(self.get_template_context())