from django.db import models from django.contrib.sites.models import Site from django.contrib.auth.models import User, Group from django.utils.translation import ugettext_lazy as _ from django.shortcuts import reverse from django.utils import timezone from django.conf import settings from rest_framework import serializers from mptt.models import MPTTModel, TreeForeignKey from recurrence.fields import RecurrenceField from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFill from ..utils import slugify from .locale import * from .profiles import * from .search import * from .. import location import re import pytz import datetime import hashlib class Place(models.Model): name = models.CharField(help_text=_('Name of the Place'), max_length=150) city = models.ForeignKey(City, verbose_name=_('City'), on_delete=models.CASCADE) address = models.CharField(help_text=_('Address with Street and Number'), max_length=150, null=True, blank=True) longitude = models.FloatField(help_text=_('Longitude in Degrees East'), null=True, blank=True) latitude = models.FloatField(help_text=_('Latitude in Degrees North'), null=True, blank=True) tz = models.CharField(max_length=32, verbose_name=_('Timezone'), default='UTC', choices=location.TimezoneChoices(), blank=False, null=False) place_url = models.URLField(help_text=_('URL for the Place Homepage'), verbose_name=_('URL of the Place'), max_length=200, blank=True, null=True) cover_img = models.URLField(_("Place photo"), null=True, blank=True) def get_absolute_url(self): return reverse('show-place', kwargs={'place_id': self.id}) def __str__(self): return u'%s, %s' % (self.name, self.city.name) class PlaceSerializer(serializers.ModelSerializer): city = serializers.CharField(read_only=True) class Meta: model = Place fields = ( 'id', 'name', 'city', 'address', 'longitude', 'latitude', 'tz', 'place_url', 'cover_img' ) class Event(models.Model): CANCELED = -1 PLANNING = 0 CONFIRMED = 1 STATUSES = [ (CANCELED, _("Canceled")), (PLANNING, _("Planning")), (CONFIRMED, _("Confirmed")), ] name = models.CharField(max_length=150, verbose_name=_('Event Name')) team = models.ForeignKey(Team, on_delete=models.CASCADE) parent = models.ForeignKey('CommonEvent', related_name='participating_events', null=True, blank=True, on_delete=models.SET_NULL) series = models.ForeignKey('EventSeries',related_name='instances', null=True, blank=True, on_delete=models.SET_NULL) status = models.SmallIntegerField(choices=STATUSES, default=CONFIRMED, db_index=True) start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True) end_time = models.DateTimeField(verbose_name=_('End Time'), db_index=True) summary = models.TextField(help_text=_('Markdown formatting supported'), blank=True, null=True) place = models.ForeignKey(Place, blank=True, null=True, on_delete=models.CASCADE) web_url = models.URLField(verbose_name=_('Website URL'), max_length=200, blank=True, null=True) announce_url = models.URLField(verbose_name=_('Announcement URL'), max_length=200, blank=True, null=True) created_by = models.ForeignKey(UserProfile, on_delete=models.CASCADE) created_time = models.DateTimeField(help_text=_('the date and time when the event was created'), default=timezone.now, db_index=True) tags = models.CharField(verbose_name=_("Keyword Tags"), blank=True, null=True, max_length=128) #image #replies attendees = models.ManyToManyField(UserProfile, through='Attendee', related_name="attending", blank=True) sponsors = models.ManyToManyField('Sponsor', related_name='events', blank=True) enable_comments = models.BooleanField(verbose_name=_('Comments'), default=True) enable_photos = models.BooleanField(verbose_name=_('Photos'), default=True) enable_presentations = models.BooleanField(verbose_name=_('Presentations'), default=False) @property def is_over(self): return self.end_time <= timezone.now() @property def tz(self): if self.place is not None: return self.place.tz elif self.team is not None: return self.team.tz else: return settings.TIME_ZONE def localize_datetime(self, val): if val is not None: event_tz = pytz.timezone(self.tz) return timezone.make_naive(val.astimezone(event_tz), event_tz) else: return None @property def local_start_time(self, val=None): if val is not None: self.start_time = val.astimezone(python.utc) else: if self.start_time is None: return None event_tz = pytz.timezone(self.tz) return timezone.make_naive(self.start_time.astimezone(event_tz), event_tz) @property def local_end_time(self, val=None): if val is not None: self.end_time = val.astimezone(python.utc) else: if self.end_time is None: return None event_tz = pytz.timezone(self.tz) return timezone.make_naive(self.end_time.astimezone(event_tz), event_tz) def get_absolute_url(self): return reverse('show-event', kwargs={'event_id': self.id, 'event_slug': self.slug}) def get_full_url(self): site = Site.objects.get(id=1) return "https://%s%s" % (site.domain, self.get_absolute_url()) @property def slug(self): return slugify(self.name) def __str__(self): return u'%s by %s at %s' % (self.name, self.team.name, self.start_time) def save(self, *args, **kwargs): super().save(*args, **kwargs) # Call the "real" save() method. if self.status > self.CANCELED: update_event_searchable(self) else: delete_event_searchable(self) def update_event_searchable(event): site = Site.objects.get(id=1) if settings.DEBUG: schema = 'http' else: schema = 'https' event_url = "%s://%s%s" % (schema, site.domain, event.get_absolute_url()) origin_url = "%s://%s%s" % (schema, site.domain, reverse('searchables')) md5 = hashlib.md5() federation_url = event_url.split('/') federation_node = '/'.join(federation_url[:3]) federation_id = '/'.join(federation_url[:5]) md5.update(bytes(federation_id, 'utf8')) event_uri = federation_node + '/' + md5.hexdigest() try: searchable = Searchable.objects.get(event_uri=event_uri) except: searchable = Searchable(event_uri) searchable.origin_node = origin_url searchable.federation_node = origin_url searchable.federation_time = timezone.now() searchable.event_url = event_url if event.team.card_img_url.startswith('http:') or event.team.card_img_url.startswith('https:'): searchable.img_url = event.team.card_img_url else: searchable.img_url = "%s://%s%s" % (schema, site.domain, event.team.card_img_url) searchable.event_title = event.name searchable.group_name = event.team.name searchable.start_time = event.start_time searchable.end_time = event.end_time searchable.tz = event.tz searchable.cost = 0 searchable.tags = event.tags if (event.place is not None): searchable.location_name = str(event.place.city) searchable.venue_name = event.place.name if event.place.longitude is not None and event.place.latitude is not None: searchable.longitude = event.place.longitude searchable.latitude = event.place.latitude elif event.place.city is not None: searchable.longitude = event.place.city.longitude searchable.latitude = event.place.city.latitude else: searchable.location_name = event.team.location_name if event.team.city is not None and (searchable.longitude is None or searchable.latitude is None): searchable.longitude = event.team.city.longitude searchable.latitude = event.team.city.latitude searchable.save() def delete_event_searchable(event): site = Site.objects.get(id=1) if settings.DEBUG: schema = 'http' else: schema = 'https' event_url = "%s://%s%s" % (schema, site.domain, event.get_absolute_url()) origin_url = "%s://%s%s" % (schema, site.domain, reverse('searchables')) md5 = hashlib.md5() federation_url = event_url.split('/') federation_node = '/'.join(federation_url[:3]) federation_id = '/'.join(federation_url[:5]) md5.update(bytes(federation_id, 'utf8')) event_uri = federation_node + '/' + md5.hexdigest() try: searchable = Searchable.objects.get(event_uri=event_uri) searchable.delete() except: pass class Attendee(models.Model): NORMAL=0 CREW=1 HOST=2 ROLES = [ (NORMAL, _("Normal")), (CREW, _("Crew")), (HOST, _("Host")) ] NO=-1 MAYBE=0 YES=1 STATUSES = [ (NO, _("No")), (MAYBE, _("Maybe")), (YES, _("Yes")), ] event = models.ForeignKey(Event, on_delete=models.CASCADE) user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) role = models.SmallIntegerField(_("Role"), choices=ROLES, default=NORMAL, db_index=True) status = models.SmallIntegerField(_("Attending"), choices=STATUSES, default=YES, db_index=True) actual = models.SmallIntegerField(_("Attended"), choices=STATUSES, default=MAYBE, db_index=True) joined_date = models.DateTimeField(default=timezone.now) last_reminded = models.DateTimeField(null=True, blank=True) @property def role_name(self): return Attendee.ROLES[self.role][1] @property def status_name(self): return Attendee.STATUSES[self.status+1][1] @property def actual_name(self): return Attendee.STATUSES[self.actual+1][1] def __str__(self): return "%s at %s" % (self.user, self.event) class EventPhoto(models.Model): event = models.ForeignKey(Event, related_name='photos', on_delete=models.CASCADE) title = models.CharField(max_length=256) caption = models.TextField(null=True, blank=True) src = models.ImageField(verbose_name=_('Photo'), upload_to='event_photos') thumbnail = ImageSpecField(source='src', processors=[ResizeToFill(250, 187)], format='JPEG', options={'quality': 60}) class EventComment(MPTTModel): REMOVED=-1 PENDING=0 APPROVED=1 STATUSES = [ (REMOVED, _("Removed")), (PENDING, _("Pending")), (APPROVED, _("Approved")), ] author = models.ForeignKey(UserProfile, on_delete=models.CASCADE) event = models.ForeignKey(Event, related_name='comments', on_delete=models.CASCADE) body = models.TextField(help_text=_('Markdown formatting supported')) created_time = models.DateTimeField(default=timezone.now, db_index=True) status = models.SmallIntegerField(choices=STATUSES, default=APPROVED, db_index=True) parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.SET_NULL) @property def local_created_time(self): return self.event.localize_datetime(self.created_time) def __str__(self): return '%s at %s' % (self.author, self.created_time) class CommonEvent(models.Model): name = models.CharField(max_length=150, verbose_name=_('Event Name')) organization = models.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE) parent = models.ForeignKey('CommonEvent', related_name='sub_events', null=True, blank=True, on_delete=models.SET_NULL) start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True) end_time = models.DateTimeField(verbose_name=_('End Time'), db_index=True) summary = models.TextField(help_text=_('Markdown formatting supported'), blank=True, null=True) continent = models.ForeignKey(Continent, null=True, blank=True, on_delete=models.SET_NULL) country = models.ForeignKey(Country, null=True, blank=True, on_delete=models.SET_NULL) spr = models.ForeignKey(SPR, null=True, blank=True, on_delete=models.SET_NULL) city = models.ForeignKey(City, null=True, blank=True, on_delete=models.SET_NULL) place = models.ForeignKey(Place, blank=True, null=True, on_delete=models.SET_NULL) web_url = models.URLField(verbose_name=_('Website URL'), max_length=200, blank=True, null=True) announce_url = models.URLField(verbose_name=_('Announcement URL'), max_length=200, blank=True, null=True) created_by = models.ForeignKey(UserProfile, on_delete=models.CASCADE) created_time = models.DateTimeField(help_text=_('the date and time when the event was created'), default=timezone.now, db_index=True) category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) topics = models.ManyToManyField('Topic', blank=True) tags = models.CharField(verbose_name=_("Keyword Tags"), blank=True, null=True, max_length=128) def get_absolute_url(self): return reverse('show-common-event', kwargs={'event_id': self.id, 'event_slug': self.slug}) def get_full_url(self): site = self.organization.site if settings.DEBUG: schema = 'http' else: schema = 'https' return "%s://%s%s" % (schema, site.domain, self.get_absolute_url()) @property def full_img_url(self): if self.organization.tile_img is not None and self.organization.tile_img .name is not None: if self.organization.tile_img .url.startswith('http'): return self.organization.tile_img .url else: site = self.organization.site if settings.DEBUG: schema = 'http' else: schema = 'https' return "%s://%s%s" % (schema, site.domain, self.organization.tile_img .url) else: return self.category.img_url def location(self): if self.city: return self.city elif self.spr: return self.spr elif self.country: return self.country elif self.continent: return self.continent else: return _('Global') @property def slug(self): return slugify(self.name) def __str__(self): return self.name class EventSeries(models.Model): class Meta: verbose_name_plural = 'Event series' name = models.CharField(max_length=150, verbose_name=_('Event Name')) team = models.ForeignKey(Team, on_delete=models.CASCADE) parent = models.ForeignKey('CommonEvent', related_name='planned_events', null=True, blank=True, on_delete=models.SET_NULL) recurrences = RecurrenceField(null=True) last_time = models.DateTimeField(help_text=_('Date and time of the last created instance in this series'), default=timezone.now, db_index=True) start_time = models.TimeField(help_text=_('Local time that the event starts'), verbose_name=_('Start Time'), db_index=True) end_time = models.TimeField(help_text=_('Local time that the event ends'), verbose_name=_('End Time'), db_index=True) summary = models.TextField(help_text=_('Summary of the Event'), blank=True, null=True) place = models.ForeignKey(Place, blank=True, null=True, on_delete=models.CASCADE) created_by = models.ForeignKey(UserProfile, on_delete=models.CASCADE) created_time = models.DateTimeField(help_text=_('the date and time when the event was created'), default=timezone.now, db_index=True) tags = models.CharField(verbose_name=_("Keyword Tags"), blank=True, null=True, max_length=128) @classmethod def from_event(klass, event, recurrences): new_series = EventSeries( team=event.team, parent=event.parent, name=event.name, start_time=event.local_start_time.time(), end_time=event.local_end_time.time(), last_time=event.start_time, summary=event.summary, place=event.place, created_by=event.created_by, recurrences=recurrences, ) return new_series def create_next_in_series(self): next_date = self.recurrences.after(self.last_time, dtstart=self.last_time) if next_date is None: return None event_tz = pytz.timezone(self.tz) next_start = pytz.utc.localize(timezone.make_naive(event_tz.localize(datetime.datetime.combine(next_date.date(), self.start_time)))) next_end = pytz.utc.localize(timezone.make_naive(event_tz.localize(datetime.datetime.combine(next_date.date(), self.end_time)))) next_event = Event( series=self, team=self.team, name=self.name, start_time=next_start, end_time=next_end, summary=self.summary, place=self.place, created_by=self.created_by, ) next_event.save() Attendee.objects.create(event=next_event, user=self.created_by, role=Attendee.HOST, status=Attendee.YES) self.last_time = next_event.start_time self.save() return next_event def get_absolute_url(self): return reverse('show-series', kwargs={'series_id': self.id, 'series_slug': self.slug}) def get_full_url(self): site = Site.objects.get(id=1) if settings.DEBUG: schema = 'http' else: schema = 'https' return "%s://%s%s" % (schema, site.domain, self.get_absolute_url()) @property def slug(self): return slugify(self.name) @property def tz(self): if self.place is not None: return self.place.tz elif self.team is not None: return self.team.tz else: return settings.TIME_ZONE def __str__(self): return u'%s by %s at %s' % (self.name, self.team.name, self.start_time)