# -*- coding: utf-8 -*-
"""
Models are modeled after schema.org.

When model is going to be serialized as JSON(-LD), model name must be same as
Schema.org schema name, the model name is automatically published in @type
JSON-LD field.
Note: jsonld_type attribute value can be used to override @type definition in
rendering phase.

Schema definitions: http://schema.org/<ModelName>
(e.g. http://schema.org/Event)

Some models have custom fields not found from schema.org. Decide if there's a
need for custom extension types (e.g. Event/MyCustomEvent) as schema.org
documentation is suggesting: http://schema.org/docs/extension.html. Override
schema_org_type can be used to define custom types. Override jsonld_context
attribute to change @context when need to define schemas for custom fields.
"""
import datetime
import logging
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
import pytz
from django.contrib.gis.db import models
from rest_framework.exceptions import ValidationError
from reversion import revisions as reversion
from django.utils.translation import ugettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from mptt.querysets import TreeQuerySet
from django.contrib.contenttypes.models import ContentType
from events import translation_utils
from django.utils.encoding import python_2_unicode_compatible
from django.contrib.postgres.fields import HStoreField
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.db import transaction
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from image_cropping import ImageRatioField
from munigeo.models import AdministrativeDivision
from notifications.models import render_notification_template, NotificationType, NotificationTemplateException
from smtplib import SMTPException

logger = logging.getLogger(__name__)

User = settings.AUTH_USER_MODEL


class PublicationStatus:
    PUBLIC = 1
    DRAFT = 2


PUBLICATION_STATUSES = (
    (PublicationStatus.PUBLIC, "public"),
    (PublicationStatus.DRAFT, "draft"),
)


class SchemalessFieldMixin(models.Model):
    custom_data = HStoreField(null=True, blank=True)

    class Meta:
        abstract = True


@python_2_unicode_compatible
class DataSource(models.Model):
    id = models.CharField(max_length=100, primary_key=True)
    name = models.CharField(verbose_name=_('Name'), max_length=255)
    api_key = models.CharField(max_length=128, blank=True, default='')
    owner = models.ForeignKey(
        'django_orghierarchy.Organization', on_delete=models.SET_NULL,
        related_name='owned_systems', null=True, blank=True)
    user_editable = models.BooleanField(default=False, verbose_name=_('Objects may be edited by users'))

    def __str__(self):
        return self.id


class SimpleValueMixin(object):
    """
    Used for models which are simple one-to-many fields
    and can be compared by value when importing as part
    of their related object. These models have no existence
    outside their related object.
    """
    def value_fields(self):
        return []

    def simple_value(self):
        field_names = translation_utils.expand_model_fields(self, self.value_fields())
        return tuple((f, getattr(self, f)) for f in field_names)

    def value_equals(self, other):
        return self.simple_value() == other.simple_value()


class BaseQuerySet(models.QuerySet):
    def is_user_editable(self):
        return not bool(self.filter(data_source__isnull=True) and
                        self.filter(data_source__user_editable=False))

    def can_be_edited_by(self, user):
        """Check if the whole queryset can be edited by the given user"""
        if user.is_superuser:
            return True
        for event in self:
            if not user.can_edit_event(event.publisher, event.publication_status):
                return False
        return True


class BaseTreeQuerySet(TreeQuerySet, BaseQuerySet):
    pass


class ReplacedByMixin():
    def _has_circular_replacement(self):
        replaced_by = self.replaced_by
        while replaced_by is not None:
            replaced_by = replaced_by.replaced_by
            if replaced_by == self:
                return True
        return False

    def get_replacement(self):
        replacement = self.replaced_by
        while replacement.replaced_by is not None:
            replacement = replacement.replaced_by
        return replacement


class License(models.Model):
    id = models.CharField(max_length=50, primary_key=True)
    name = models.CharField(verbose_name=_('Name'), max_length=255)
    url = models.URLField(verbose_name=_('Url'), blank=True)

    class Meta:
        verbose_name = _('License')
        verbose_name_plural = _('Licenses')

    def __str__(self):
        return self.name


class Image(models.Model):
    jsonld_type = 'ImageObject'
    objects = BaseQuerySet.as_manager()

    # Properties from schema.org/Thing
    name = models.CharField(verbose_name=_('Name'), max_length=255, db_index=True, default='')

    data_source = models.ForeignKey(
        DataSource, on_delete=models.CASCADE, related_name='provided_%(class)s_data', db_index=True, null=True)
    publisher = models.ForeignKey(
        'django_orghierarchy.Organization', on_delete=models.CASCADE, verbose_name=_('Publisher'),
        db_index=True, null=True, blank=True, related_name='Published_images')

    created_time = models.DateTimeField(auto_now_add=True)
    last_modified_time = models.DateTimeField(auto_now=True, db_index=True)
    created_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True, related_name='EventImage_created_by')
    last_modified_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, related_name='EventImage_last_modified_by', null=True, blank=True)

    image = models.ImageField(upload_to='images', null=True, blank=True)
    url = models.URLField(verbose_name=_('Image'), max_length=400, null=True, blank=True)
    cropping = ImageRatioField('image', '800x800', verbose_name=_('Cropping'))
    license = models.ForeignKey(
        License, on_delete=models.SET_NULL, verbose_name=_('License'), related_name='images', default='cc_by',
        null=True)
    photographer_name = models.CharField(verbose_name=_('Photographer name'), max_length=255, null=True, blank=True)
    alt_text = models.CharField(verbose_name=_('Alt text'), max_length=320, null=True, blank=True)

    def save(self, *args, **kwargs):
        if not self.publisher:
            try:
                self.publisher = self.created_by.get_default_organization()
            except AttributeError:
                pass
        # ensure that either image or url is provided
        if not self.url and not self.image:
            raise ValidationError(_('You must provide either image or url.'))
        if self.url and self.image:
            raise ValidationError(_('You can only provide image or url, not both.'))
        self.last_modified_time = BaseModel.now()
        super(Image, self).save(*args, **kwargs)

    def is_user_editable(self):
        return bool(self.data_source and self.data_source.user_editable)

    def is_user_edited(self):
        return bool(self.is_user_editable() and self.last_modified_by)

    def can_be_edited_by(self, user):
        """Check if current image can be edited by the given user"""
        if user.is_superuser:
            return True
        return user.is_admin(self.publisher)


class ImageMixin(models.Model):
    image = models.ForeignKey(Image, verbose_name=_('Image'), on_delete=models.SET_NULL,
                              null=True, blank=True)

    class Meta:
        abstract = True


@python_2_unicode_compatible
class BaseModel(models.Model):
    objects = BaseQuerySet.as_manager()

    id = models.CharField(max_length=100, primary_key=True)
    data_source = models.ForeignKey(
        DataSource, on_delete=models.CASCADE, related_name='provided_%(class)s_data', db_index=True)

    # Properties from schema.org/Thing
    name = models.CharField(verbose_name=_('Name'), max_length=255, db_index=True)

    origin_id = models.CharField(verbose_name=_('Origin ID'), max_length=100, db_index=True, null=True,
                                 blank=True)

    created_time = models.DateTimeField(null=True, blank=True, auto_now_add=True)
    last_modified_time = models.DateTimeField(null=True, blank=True, auto_now=True, db_index=True)

    created_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name="%(app_label)s_%(class)s_created_by")
    last_modified_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name="%(app_label)s_%(class)s_modified_by")

    @staticmethod
    def now():
        return datetime.datetime.utcnow().replace(tzinfo=pytz.utc)

    def __str__(self):
        return self.name

    class Meta:
        abstract = True

    def is_user_editable(self):
        return self.data_source.user_editable

    def is_user_edited(self):
        return bool(self.data_source.user_editable and self.last_modified_by)


class Language(models.Model):
    id = models.CharField(max_length=10, primary_key=True)
    name = models.CharField(verbose_name=_('Name'), max_length=20)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = _('language')
        verbose_name_plural = _('languages')


class KeywordLabel(models.Model):
    name = models.CharField(verbose_name=_('Name'), max_length=255, db_index=True)
    language = models.ForeignKey(Language, on_delete=models.CASCADE, blank=False, null=False)

    def __str__(self):
        return self.name + ' (' + str(self.language) + ')'

    class Meta:
        unique_together = (('name', 'language'),)


class UpcomingEventsUpdater(models.Manager):
    def has_upcoming_events_update(self):
        now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
        qs = self.model.objects.filter(n_events__gte=1)
        if self.model.__name__ == 'Keyword':
            qs = qs.filter(deprecated=False)
        elif self.model.__name__ == 'Place':
            qs = qs.filter(deleted=False)
        qs.filter(events__start_time__gte=now).update(has_upcoming_events=True)
        qs.exclude(events__start_time__gte=now).update(has_upcoming_events=False)


class Keyword(BaseModel, ImageMixin, ReplacedByMixin):
    publisher = models.ForeignKey(
        'django_orghierarchy.Organization', on_delete=models.CASCADE, verbose_name=_('Publisher'),
        db_index=True, null=True, blank=True,
        related_name='Published_keywords')
    alt_labels = models.ManyToManyField(KeywordLabel, blank=True, related_name='keywords')
    aggregate = models.BooleanField(default=False)
    deprecated = models.BooleanField(default=False, db_index=True)
    has_upcoming_events = models.BooleanField(default=False, db_index=True)
    n_events = models.IntegerField(
        verbose_name=_('event count'),
        help_text=_('number of events with this keyword'),
        default=0,
        editable=False,
        db_index=True
    )
    n_events_changed = models.BooleanField(default=False, db_index=True)
    replaced_by = models.ForeignKey(
        'Keyword', on_delete=models.SET_NULL, related_name='aliases', null=True, blank=True)

    schema_org_type = "Thing/LinkedEventKeyword"

    objects = UpcomingEventsUpdater()

    def __str__(self):
        return self.name

    def deprecate(self):
        self.deprecated = True
        self.save(update_fields=['deprecated'])
        return True

    def replace(self, replaced_by):
        self.replaced_by = replaced_by
        self.save(update_fields=['replaced_by'])
        return True

    @transaction.atomic
    def save(self, *args, **kwargs):
        if self._has_circular_replacement():
            raise ValidationError(_("Trying to replace this keyword with a keyword that is replaced by this keyword. "
                                    "Please refrain from creating circular replacements and"
                                    "remove one of the replacements."))

        if self.replaced_by and not self.deprecated:
            self.deprecated = True
            logger.warning("Keyword replaced without deprecating. Deprecating automatically", extra={'keyword': self})

        old_replaced_by = None
        if self.id:
            try:
                old_replaced_by = Keyword.objects.get(id=self.id).replaced_by
            except Keyword.DoesNotExist:
                pass

        super().save(*args, **kwargs)

        if not old_replaced_by == self.replaced_by:
            # Remap keyword sets
            qs = KeywordSet.objects.filter(keywords__id__exact=self.id)
            for kw_set in qs:
                kw_set.keywords.remove(self)
                kw_set.keywords.add(self.replaced_by)
                kw_set.save()

            # Remap events
            qs = Event.objects.filter(keywords__id__exact=self.id) \
                | Event.objects.filter(audience__id__exact=self.id)
            for event in qs:
                if self in event.keywords.all():
                    event.keywords.remove(self)
                    event.keywords.add(self.replaced_by)
                if self in event.audience.all():
                    event.audience.remove(self)
                    event.audience.add(self.replaced_by)

    class Meta:
        verbose_name = _('keyword')
        verbose_name_plural = _('keywords')


class KeywordSet(BaseModel, ImageMixin):
    """
    Sets of pre-chosen keywords intended or specific uses and/or organizations,
    for example the set of possible audiences for an event in a specific client.
    """

    ANY = 1
    KEYWORD = 2
    AUDIENCE = 3

    USAGES = (
        (ANY, "any"),
        (KEYWORD, "keyword"),
        (AUDIENCE, "audience"),
    )
    usage = models.SmallIntegerField(verbose_name=_('Intended keyword usage'), choices=USAGES, default=ANY)
    organization = models.ForeignKey('django_orghierarchy.Organization', on_delete=models.CASCADE,
                                     verbose_name=_('Organization which uses this set'), null=True)
    keywords = models.ManyToManyField(Keyword, blank=False, related_name='sets')

    def save(self, *args, **kwargs):
        if any([keyword.deprecated for keyword in self.keywords.all()]):
            raise ValidationError(_("KeywordSet can't have deprecated keywords"))
        super().save(*args, **kwargs)


class Place(MPTTModel, BaseModel, SchemalessFieldMixin, ImageMixin, ReplacedByMixin):
    objects = BaseTreeQuerySet.as_manager()
    upcoming_events = UpcomingEventsUpdater()

    geo_objects = objects

    publisher = models.ForeignKey(
        'django_orghierarchy.Organization', on_delete=models.CASCADE, verbose_name=_('Publisher'), db_index=True)
    info_url = models.URLField(verbose_name=_('Place home page'), null=True, blank=True, max_length=1000)
    description = models.TextField(verbose_name=_('Description'), null=True, blank=True)
    parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True,
                            related_name='children')

    position = models.PointField(srid=settings.PROJECTION_SRID, null=True,
                                 blank=True)

    email = models.EmailField(verbose_name=_('E-mail'), null=True, blank=True)
    telephone = models.CharField(verbose_name=_('Telephone'), max_length=128, null=True, blank=True)
    contact_type = models.CharField(verbose_name=_('Contact type'), max_length=255, null=True, blank=True)
    street_address = models.CharField(verbose_name=_('Street address'), max_length=255, null=True, blank=True)
    address_locality = models.CharField(verbose_name=_('Address locality'), max_length=255, null=True, blank=True)
    address_region = models.CharField(verbose_name=_('Address region'), max_length=255, null=True, blank=True)
    postal_code = models.CharField(verbose_name=_('Postal code'), max_length=128, null=True, blank=True)
    post_office_box_num = models.CharField(verbose_name=_('PO BOX'), max_length=128, null=True,
                                           blank=True)
    address_country = models.CharField(verbose_name=_('Country'), max_length=2, null=True, blank=True)

    deleted = models.BooleanField(verbose_name=_('Deleted'), default=False)
    replaced_by = models.ForeignKey('Place', on_delete=models.SET_NULL, related_name='aliases', null=True, blank=True)
    divisions = models.ManyToManyField(AdministrativeDivision, verbose_name=_('Divisions'), related_name='places',
                                       blank=True)
    has_upcoming_events = models.BooleanField(default=False, db_index=True)
    n_events = models.IntegerField(
        verbose_name=_('event count'),
        help_text=_('number of events in this location'),
        default=0,
        editable=False,
        db_index=True
    )
    n_events_changed = models.BooleanField(default=False, db_index=True)

    class Meta:
        verbose_name = _('place')
        verbose_name_plural = _('places')
        unique_together = (('data_source', 'origin_id'),)

    def __unicode__(self):
        values = filter(lambda x: x, [
            self.street_address, self.postal_code, self.address_locality
        ])
        return u', '.join(values)

    @transaction.atomic
    def save(self, *args, **kwargs):
        if self._has_circular_replacement():
            raise ValidationError(_("Trying to replace this place with a place that is replaced by this place. "
                                    "Please refrain from creating circular replacements and remove one of the "
                                    "replacements. We don't want homeless events."))

        if self.replaced_by and not self.deleted:
            self.deleted = True
            logger.warning("Place replaced without soft deleting. Soft deleting automatically", extra={'place': self})

        # needed to remap events to replaced location
        old_replaced_by = None
        if self.id:
            try:
                old_replaced_by = Place.objects.get(id=self.id).replaced_by
            except Place.DoesNotExist:
                pass

        super().save(*args, **kwargs)

        # needed to remap events to replaced location
        if not old_replaced_by == self.replaced_by:
            Event.objects.filter(location=self).update(location=self.replaced_by)
            # Update doesn't call save so we update event numbers manually.
            # Not all of the below are necessarily present.
            ids_to_update = [event.id for event in (self, self.replaced_by, old_replaced_by) if event]
            Place.objects.filter(id__in=ids_to_update).update(n_events_changed=True)

        if self.position:
            self.divisions.set(AdministrativeDivision.objects.filter(
                type__type__in=('district', 'sub_district', 'neighborhood', 'muni'),
                geometry__boundary__contains=self.position))
        else:
            self.divisions.clear()


reversion.register(Place)


class OpeningHoursSpecification(models.Model):
    GR_BASE_URL = "http://purl.org/goodrelations/v1#"
    WEEK_DAYS = (
        (1, "Monday"), (2, "Tuesday"), (3, "Wednesday"), (4, "Thursday"),
        (5, "Friday"), (6, "Saturday"), (7, "Sunday"), (8, "PublicHolidays")
    )

    place = models.ForeignKey(Place, on_delete=models.CASCADE, db_index=True,
                              related_name='opening_hours')
    opens = models.TimeField(null=True, blank=True)
    closes = models.TimeField(null=True, blank=True)
    days_of_week = models.SmallIntegerField(choices=WEEK_DAYS, null=True,
                                            blank=True)
    valid_from = models.DateTimeField(null=True, blank=True)
    valid_through = models.DateTimeField(null=True, blank=True)

    class Meta:
        verbose_name = _('opening hour specification')
        verbose_name_plural = _('opening hour specifications')


class Event(MPTTModel, BaseModel, SchemalessFieldMixin, ReplacedByMixin):
    jsonld_type = "Event/LinkedEvent"
    objects = BaseTreeQuerySet.as_manager()

    """
    eventStatus enumeration is based on http://schema.org/EventStatusType
    """

    class Status:
        SCHEDULED = 1
        CANCELLED = 2
        POSTPONED = 3
        RESCHEDULED = 4
    # Properties from schema.org/Event
    STATUSES = (
        (Status.SCHEDULED, "EventScheduled"),
        (Status.CANCELLED, "EventCancelled"),
        (Status.POSTPONED, "EventPostponed"),
        (Status.RESCHEDULED, "EventRescheduled"),
    )

    class SuperEventType:
        RECURRING = 'recurring'
        UMBRELLA = 'umbrella'

    SUPER_EVENT_TYPES = (
        (SuperEventType.RECURRING, _('Recurring')),
        (SuperEventType.UMBRELLA, _('Umbrella event')),
    )

    # Properties from schema.org/Thing
    info_url = models.URLField(verbose_name=_('Event home page'), blank=True, null=True, max_length=1000)
    description = models.TextField(verbose_name=_('Description'), blank=True, null=True)
    short_description = models.TextField(verbose_name=_('Short description'), blank=True, null=True)

    # Properties from schema.org/CreativeWork
    date_published = models.DateTimeField(verbose_name=_('Date published'), null=True, blank=True)
    # headline and secondary_headline are for cases where
    # the original event data contains a title and a subtitle - in that
    # case the name field is combined from these.
    #
    # secondary_headline is mapped to schema.org alternative_headline
    # and is used for subtitles, that is for
    # secondary, complementary headlines, not "alternative" headlines
    headline = models.CharField(verbose_name=_('Headline'), max_length=255, null=True, db_index=True)
    secondary_headline = models.CharField(verbose_name=_('Secondary headline'), max_length=255,
                                          null=True, db_index=True)
    provider = models.CharField(verbose_name=_('Provider'), max_length=512, null=True)
    provider_contact_info = models.CharField(verbose_name=_("Provider's contact info"),
                                             max_length=255, null=True, blank=True)
    publisher = models.ForeignKey('django_orghierarchy.Organization', verbose_name=_('Publisher'), db_index=True,
                                  on_delete=models.PROTECT, related_name='published_events')

    # Status of the event itself
    event_status = models.SmallIntegerField(verbose_name=_('Event status'), choices=STATUSES,
                                            default=Status.SCHEDULED)

    # Whether or not this data about the event is ready to be viewed by the general public.
    # DRAFT means the data is considered incomplete or is otherwise undergoing refinement --
    # or just waiting to be published for other reasons.
    publication_status = models.SmallIntegerField(
        verbose_name=_('Event data publication status'), choices=PUBLICATION_STATUSES,
        default=PublicationStatus.PUBLIC)

    location = models.ForeignKey(Place, related_name='events', null=True, blank=True, on_delete=models.PROTECT)
    location_extra_info = models.CharField(verbose_name=_('Location extra info'),
                                           max_length=400, null=True, blank=True)

    start_time = models.DateTimeField(verbose_name=_('Start time'), null=True, db_index=True, blank=True)
    end_time = models.DateTimeField(verbose_name=_('End time'), null=True, db_index=True, blank=True)
    has_start_time = models.BooleanField(default=True)
    has_end_time = models.BooleanField(default=True)

    audience_min_age = models.SmallIntegerField(verbose_name=_('Minimum recommended age'),
                                                blank=True, null=True, db_index=True)
    audience_max_age = models.SmallIntegerField(verbose_name=_('Maximum recommended age'),
                                                blank=True, null=True, db_index=True)

    super_event = TreeForeignKey('self', null=True, blank=True,
                                 on_delete=models.SET_NULL, related_name='sub_events')

    super_event_type = models.CharField(max_length=255, blank=True, null=True, db_index=True,
                                        default=None, choices=SUPER_EVENT_TYPES)

    in_language = models.ManyToManyField(Language, verbose_name=_('In language'), related_name='events', blank=True)

    images = models.ManyToManyField(Image, related_name='events', blank=True)

    deleted = models.BooleanField(default=False, db_index=True)

    replaced_by = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='aliases', null=True, blank=True)

    # Custom fields not from schema.org
    keywords = models.ManyToManyField(Keyword, related_name='events')
    audience = models.ManyToManyField(Keyword, related_name='audience_events', blank=True)

    class Meta:
        verbose_name = _('event')
        verbose_name_plural = _('events')

    class MPTTMeta:
        parent_attr = 'super_event'

    def save(self, *args, **kwargs):
        if self._has_circular_replacement():
            raise ValidationError(_("Trying to replace this event with an event that is replaced by this event. "
                                    "Please refrain from creating circular replacements and "
                                    "remove one of the replacements."))

        if self.replaced_by and not self.deleted:
            self.deleted = True
            logger.warning("Event replaced without soft deleting. Soft deleting automatically", extra={'event': self})

        # needed to cache location event numbers
        old_location = None

        # needed for notifications
        old_publication_status = None
        old_deleted = None
        created = True

        if self.id:
            try:
                event = Event.objects.get(id=self.id)
                created = False
                old_location = event.location
                old_publication_status = event.publication_status
                old_deleted = event.deleted
            except Event.DoesNotExist:
                pass

        # drafts may not have times set, so check that first
        start = getattr(self, 'start_time', None)
        end = getattr(self, 'end_time', None)
        if start and end:
            if start > end:
                raise ValidationError({'end_time': _('The event end time cannot be earlier than the start time.')})

        if (self.keywords.filter(deprecated=True) or self.audience.filter(deprecated=True)) and (
                not self.deleted):
            raise ValidationError({'keywords': _("Trying to save event with deprecated keywords " +
                                                 str(self.keywords.filter(deprecated=True).values('id')) + " or " +
                                                 str(self.audience.filter(deprecated=True).values('id')) +
                                                 ". Please use up-to-date keywords.")})

        super(Event, self).save(*args, **kwargs)

        # needed to cache location event numbers
        if not old_location and self.location:
            Place.objects.filter(id=self.location.id).update(n_events_changed=True)
        if old_location and not self.location:
            # drafts (or imported events) may not always have location set
            Place.objects.filter(id=old_location.id).update(n_events_changed=True)
        if old_location and self.location and old_location != self.location:
            Place.objects.filter(id__in=(old_location.id, self.location.id)).update(n_events_changed=True)

        # send notifications
        if old_publication_status == PublicationStatus.DRAFT and self.publication_status == PublicationStatus.PUBLIC:
            self.send_published_notification()
        if self.publication_status == PublicationStatus.DRAFT and (old_deleted is False and self.deleted is True):
            self.send_deleted_notification()
        if created and self.publication_status == PublicationStatus.DRAFT:
            self.send_draft_posted_notification()

    def __str__(self):
        name = ''
        languages = [lang[0] for lang in settings.LANGUAGES]
        for lang in languages:
            lang = lang.replace('-', '_')  # to handle complex codes like e.g. zh-hans
            s = getattr(self, 'name_%s' % lang, None)
            if s:
                name = s
                break
        val = [name, '(%s)' % self.id]
        dcount = self.get_descendant_count()
        if dcount > 0:
            val.append(u" (%d children)" % dcount)
        else:
            val.append(str(self.start_time))
        return u" ".join(val)

    def is_admin(self, user):
        if user.is_superuser:
            return True
        else:
            return user.is_admin(self.publisher)

    def can_be_edited_by(self, user):
        """Check if current event can be edited by the given user"""
        if user.is_superuser:
            return True
        return user.can_edit_event(self.publisher, self.publication_status)

    def soft_delete(self, using=None):
        self.deleted = True
        self.save(update_fields=("deleted",), using=using, force_update=True)

    def undelete(self, using=None):
        self.deleted = False
        self.save(update_fields=("deleted",), using=using, force_update=True)

    def _send_notification(self, notification_type, recipient_list, request=None):
        if len(recipient_list) == 0:
            logger.warning("No recipients for notification type '%s'" % notification_type, extra={'event': self})
            return
        context = {'event': self}
        try:
            rendered_notification = render_notification_template(notification_type, context)
        except NotificationTemplateException as e:
            logger.error(e, exc_info=True, extra={'request': request})
            return
        try:
            send_mail(
                rendered_notification['subject'],
                rendered_notification['body'],
                'noreply@%s' % Site.objects.get_current().domain,
                recipient_list,
                html_message=rendered_notification['html_body']
            )
        except SMTPException as e:
            logger.error(e, exc_info=True, extra={'request': request, 'event': self})

    def _get_author_emails(self):
        author_emails = []
        author = self.created_by
        if author and author.email:
            author_emails.append(author.email)
        return author_emails

    def send_deleted_notification(self, request=None):
        recipient_list = self._get_author_emails()
        self._send_notification(NotificationType.UNPUBLISHED_EVENT_DELETED, recipient_list, request)

    def send_published_notification(self, request=None):
        recipient_list = self._get_author_emails()
        self._send_notification(NotificationType.EVENT_PUBLISHED, recipient_list, request)

    def send_draft_posted_notification(self, request=None):
        recipient_list = []
        for admin in self.publisher.admin_users.all():
            if admin.email:
                recipient_list.append(admin.email)
        self._send_notification(NotificationType.DRAFT_POSTED, recipient_list, request)


reversion.register(Event)


@receiver(m2m_changed, sender=Event.keywords.through)
@receiver(m2m_changed, sender=Event.audience.through)
def keyword_added_or_removed(sender, model=None,
                             instance=None, pk_set=None, action=None, **kwargs):
    """
    Listens to event-keyword add signals to keep event number up to date
    """
    if action in ('post_add', 'post_remove'):
        if model is Keyword:
            Keyword.objects.filter(pk__in=pk_set).update(n_events_changed=True)
        if model is Event:
            instance.n_events_changed = True
            instance.save(update_fields=("n_events_changed",))


class Offer(models.Model, SimpleValueMixin):
    event = models.ForeignKey(Event, on_delete=models.CASCADE, db_index=True, related_name='offers')
    price = models.CharField(verbose_name=_('Price'), blank=True, max_length=1000)
    info_url = models.URLField(verbose_name=_('Web link to offer'), blank=True, null=True, max_length=1000)
    description = models.TextField(verbose_name=_('Offer description'), blank=True, null=True)
    # Don't expose is_free as an API field. It is used to distinguish
    # between missing price info and confirmed free entry.
    is_free = models.BooleanField(verbose_name=_('Is free'), default=False)

    def value_fields(self):
        return ['price', 'info_url', 'description', 'is_free']


reversion.register(Offer)


class EventLink(models.Model, SimpleValueMixin):
    name = models.CharField(verbose_name=_('Name'), max_length=100, blank=True)
    event = models.ForeignKey(Event, on_delete=models.CASCADE, db_index=True, related_name='external_links')
    language = models.ForeignKey(Language, on_delete=models.CASCADE)
    link = models.URLField()

    class Meta:
        unique_together = (('name', 'event', 'language', 'link'),)

    def value_fields(self):
        return ['name', 'language_id', 'link']


class Video(models.Model, SimpleValueMixin):
    name = models.CharField(verbose_name=_('Name'), max_length=255, db_index=True, default='')
    event = models.ForeignKey(Event, on_delete=models.CASCADE, db_index=True, related_name='videos')
    url = models.URLField()
    alt_text = models.CharField(verbose_name=_('Alt text'), max_length=320, null=True, blank=True)

    class Meta:
        unique_together = (('name', 'event', 'url'),)

    def value_fields(self):
        return ['name', 'url']


class ExportInfo(models.Model):
    target_id = models.CharField(max_length=255, db_index=True, null=True,
                                 blank=True)
    target_system = models.CharField(max_length=255, db_index=True, null=True,
                                     blank=True)
    last_exported_time = models.DateTimeField(null=True, blank=True)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.CharField(max_length=50)
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        unique_together = (('target_system', 'content_type', 'object_id'),)

    def save(self, *args, **kwargs):
        self.last_exported_time = BaseModel.now()
        super(ExportInfo, self).save(*args, **kwargs)


class EventAggregate(models.Model):
    super_event = models.OneToOneField(Event, on_delete=models.CASCADE, related_name='aggregate', null=True)


class EventAggregateMember(models.Model):
    event_aggregate = models.ForeignKey(EventAggregate, on_delete=models.CASCADE, related_name='members')
    event = models.OneToOneField(Event, on_delete=models.CASCADE)