Convert Event start and end times to UTC, display them in local time but store them in UTC. Show ical feeds for the user and teams. Fixes #28, Fixes #59
This commit is contained in:
parent
2e21c5789e
commit
d440e5b173
29 changed files with 493 additions and 47 deletions
66
events/feeds.py
Normal file
66
events/feeds.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from django.contrib.sites.models import Site
|
||||
from django_ical.views import ICalFeed
|
||||
from django.utils import timezone
|
||||
|
||||
import datetime
|
||||
|
||||
from .models.events import Event, CommonEvent
|
||||
from .models.profiles import UserProfile, Team, Organization
|
||||
|
||||
class AbstractEventCalendarFeed(ICalFeed):
|
||||
def item_guid(self, event):
|
||||
site = Site.objects.get(id=1)
|
||||
return '%s@%s' % (event.id, site.domain)
|
||||
|
||||
def item_link(self, event):
|
||||
return event.get_full_url()
|
||||
|
||||
def item_title(self, event):
|
||||
return event.name
|
||||
|
||||
def item_description(self, event):
|
||||
return event.summary
|
||||
|
||||
def item_start_datetime(self, event):
|
||||
return event.start_time
|
||||
|
||||
def item_end_datetime(self, event):
|
||||
return event.end_time
|
||||
|
||||
def item_created(self, event):
|
||||
return event.created_time
|
||||
|
||||
def item_location(self, event):
|
||||
if event.place is not None:
|
||||
return str(event.place)
|
||||
return None
|
||||
|
||||
def item_geo(self, event):
|
||||
if event.place is not None:
|
||||
latitude = event.place.latitude or None
|
||||
longitude = event.place.longitude or None
|
||||
return (latitude, longitude)
|
||||
elif event.team.city is not None:
|
||||
latitude = event.team.city.latitude
|
||||
longitude = event.team.city.longitude
|
||||
return (latitude, longitude)
|
||||
return None
|
||||
|
||||
class UserEventsCalendar(AbstractEventCalendarFeed):
|
||||
timezone = 'UTC'
|
||||
|
||||
def get_object(self, request, account_secret):
|
||||
return UserProfile.objects.get(secret_key=account_secret)
|
||||
|
||||
def items(self, user):
|
||||
return Event.objects.filter(attendees=user, end_time__gt=timezone.now()).order_by('-start_time')
|
||||
|
||||
class TeamEventsCalendar(AbstractEventCalendarFeed):
|
||||
timezone = 'UTC'
|
||||
|
||||
def get_object(self, request, team_id):
|
||||
return Team.objects.get(id=team_id)
|
||||
|
||||
def items(self, team):
|
||||
return Event.objects.filter(team=team, end_time__gt=timezone.now()).order_by('-start_time')
|
||||
|
|
@ -2,12 +2,14 @@ from django.utils.safestring import mark_safe
|
|||
from django import forms
|
||||
from django.forms.widgets import TextInput, Media
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from .models.locale import Country, SPR, City
|
||||
from .models.profiles import Team, UserProfile
|
||||
from .models.events import Event, EventComment ,CommonEvent, Place, EventPhoto
|
||||
|
||||
import pytz
|
||||
from datetime import time
|
||||
from time import strptime, strftime
|
||||
|
||||
|
@ -171,6 +173,20 @@ class TeamEventForm(forms.ModelForm):
|
|||
'start_time': DateTimeWidget,
|
||||
'end_time': DateTimeWidget
|
||||
}
|
||||
def __init__(self, *args, **kargs):
|
||||
super().__init__(*args, **kargs)
|
||||
event_tz = pytz.timezone(self.instance.tz)
|
||||
if self.instance.local_start_time: self.initial['start_time'] = self.instance.local_start_time
|
||||
if self.instance.local_end_time: self.initial['end_time'] = self.instance.local_end_time
|
||||
print("Initial: %s" % self.initial)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
event_tz = pytz.timezone(self.instance.tz)
|
||||
print("Clean: %s" % cleaned_data)
|
||||
cleaned_data['start_time'] = pytz.utc.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(cleaned_data['start_time']))))
|
||||
cleaned_data['end_time'] = pytz.utc.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(cleaned_data['end_time']))))
|
||||
return cleaned_data
|
||||
|
||||
class NewTeamEventForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@ -180,6 +196,20 @@ class NewTeamEventForm(forms.ModelForm):
|
|||
'start_time': DateTimeWidget,
|
||||
'end_time': DateTimeWidget
|
||||
}
|
||||
def __init__(self, *args, **kargs):
|
||||
super().__init__(*args, **kargs)
|
||||
event_tz = pytz.timezone(self.instance.tz)
|
||||
if self.instance.local_start_time: self.initial['start_time'] = self.instance.local_start_time
|
||||
if self.instance.local_end_time: self.initial['end_time'] = self.instance.local_end_time
|
||||
print("Initial: %s" % self.initial)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
event_tz = pytz.timezone(self.instance.tz)
|
||||
print("Clean: %s" % cleaned_data)
|
||||
cleaned_data['start_time'] = pytz.utc.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(cleaned_data['start_time']))))
|
||||
cleaned_data['end_time'] = pytz.utc.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(cleaned_data['end_time']))))
|
||||
return cleaned_data
|
||||
|
||||
class DeleteEventForm(forms.Form):
|
||||
confirm = forms.BooleanField(label="Yes, delete event", required=True)
|
||||
|
|
79
events/location.py
Normal file
79
events/location.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
import math
|
||||
import pytz
|
||||
import datetime
|
||||
import geocoder
|
||||
|
||||
KM_PER_DEGREE_LAT = 110.574
|
||||
KM_PER_DEGREE_LNG = 111.320 # At the equator
|
||||
|
||||
class TimezoneChoices():
|
||||
|
||||
def __iter__(self):
|
||||
for tz in pytz.all_timezones:
|
||||
yield (tz, tz)
|
||||
|
||||
def get_client_ip(request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
def get_geoip(request):
|
||||
client_ip = get_client_ip(request)
|
||||
if client_ip == '127.0.0.1' or client_ip == 'localhost':
|
||||
if settings.DEBUG:
|
||||
client_ip = '8.8.8.8' # Try Google's server
|
||||
print("Client is localhost, using 8.8.8.8 for geoip instead")
|
||||
else:
|
||||
raise Exception("Client is localhost")
|
||||
|
||||
g = geocoder.ip(client_ip)
|
||||
return g
|
||||
|
||||
def get_bounding_box(center, radius):
|
||||
minlat = center[0]-(radius/KM_PER_DEGREE_LAT)
|
||||
maxlat = center[0]+(radius/KM_PER_DEGREE_LAT)
|
||||
minlng = center[1]-(radius/(KM_PER_DEGREE_LNG*math.cos(math.radians(center[0]))))
|
||||
maxlng = center[1]+(radius/(KM_PER_DEGREE_LNG*math.cos(math.radians(center[0]))))
|
||||
return (minlat, maxlat, minlng, maxlng)
|
||||
|
||||
def distance(center1, center2):
|
||||
avglat = (center2[0] + center1[0])/2
|
||||
dlat = (center2[0] - center1[0]) * KM_PER_DEGREE_LAT
|
||||
dlng = (center2[1] - center1[1]) * (KM_PER_DEGREE_LNG*math.cos(math.radians(avglat)))
|
||||
dkm = math.sqrt((dlat*dlat) + (dlng*dlng))
|
||||
print("Distance between %s and %s is %s" % (center1, center2, dkm))
|
||||
return dkm
|
||||
|
||||
def city_distance_from(ll, city):
|
||||
if city is not None and city.latitude is not None and city.longitude is not None:
|
||||
return distance((ll), (city.latitude, city.longitude))
|
||||
else:
|
||||
return 99999
|
||||
|
||||
def team_distance_from(ll, team):
|
||||
if team.city is not None:
|
||||
return city_distance_from(ll, team.city)
|
||||
else:
|
||||
return 99999
|
||||
|
||||
def event_distance_from(ll, event):
|
||||
if event.place is not None and event.place.latitude is not None and event.place.longitude is not None:
|
||||
return distance((ll), (event.place.latitude, event.place.longitude))
|
||||
if event.team is not None:
|
||||
return team_distance_from(ll, event.team)
|
||||
else:
|
||||
return 99999
|
||||
|
||||
def searchable_distance_from(ll, searchable):
|
||||
if searchable.latitude is not None and searchable.longitude is not None:
|
||||
return distance((ll), (float(searchable.latitude), float(searchable.longitude)))
|
||||
else:
|
||||
return 99999
|
||||
|
||||
|
12
events/management/commands/recreate_searchables.py
Normal file
12
events/management/commands/recreate_searchables.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from events.models.events import Event, update_event_searchable
|
||||
import urllib
|
||||
import datetime
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Regenerated Searchable records from this node'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for event in Event.objects.all():
|
||||
update_event_searchable(event)
|
31
events/migrations/0021_add_account_secret.py
Normal file
31
events/migrations/0021_add_account_secret.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 2.0 on 2018-03-27 15:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
def gen_unique_keys(apps, schema_editor):
|
||||
UserProfile = apps.get_model('events', 'UserProfile')
|
||||
for profile in UserProfile.objects.all():
|
||||
profile.secret_key = uuid.uuid4()
|
||||
profile.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('events', '0020_add_event_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='secret_key',
|
||||
field=models.UUIDField(default=uuid.uuid4),
|
||||
),
|
||||
migrations.RunPython(gen_unique_keys, reverse_code=migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='eventcomment',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='events.Event'),
|
||||
),
|
||||
]
|
47
events/migrations/0022_localize_datetimes.py
Normal file
47
events/migrations/0022_localize_datetimes.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 2.0 on 2018-04-01 15:11
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
|
||||
import pytz
|
||||
|
||||
def localize_event_datetimes(apps, schema_editor):
|
||||
Event = MyModel = apps.get_model('events', 'Event')
|
||||
for event in Event.objects.all():
|
||||
utc_tz = pytz.timezone('UTC')
|
||||
event_tz = get_event_timezone(event)
|
||||
print("Converting event %s to %s" % (event.id, event_tz))
|
||||
event.start_time = utc_tz.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(event.start_time, pytz.utc))))
|
||||
event.end_time = utc_tz.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(event.end_time, pytz.utc))))
|
||||
event.save()
|
||||
|
||||
def localize_commonevent_datetimes(apps, schema_editor):
|
||||
CommonEvent = MyModel = apps.get_model('events', 'CommonEvent')
|
||||
for event in CommonEvent.objects.all():
|
||||
utc_tz = pytz.timezone('UTC')
|
||||
event_tz = get_event_timezone(event)
|
||||
print("Converting common event %s to %s" % (event.id, event_tz))
|
||||
event.start_time = utc_tz.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(event.start_time, pytz.utc))))
|
||||
event.end_time = utc_tz.localize(timezone.make_naive(event_tz.localize(timezone.make_naive(event.end_time, pytz.utc))))
|
||||
event.save()
|
||||
|
||||
def get_event_timezone(event):
|
||||
if event.place is not None:
|
||||
return pytz.timezone(event.place.tz)
|
||||
elif hasattr(event, 'team') and event.team is not None:
|
||||
return pytz.timezone(event.team.tz)
|
||||
elif hasattr(event, 'city') and event.city is not None:
|
||||
return pytz.timezone(event.city.tz)
|
||||
else:
|
||||
return pytz.timezone("UTC")
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('events', '0021_add_account_secret'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(localize_event_datetimes, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RunPython(localize_commonevent_datetimes, reverse_code=migrations.RunPython.noop),
|
||||
]
|
64
events/migrations/0023_add_searchable_timezone.py
Normal file
64
events/migrations/0023_add_searchable_timezone.py
Normal file
File diff suppressed because one or more lines are too long
|
@ -3,6 +3,7 @@ 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 rest_framework import serializers
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
@ -13,6 +14,7 @@ from imagekit.processors import ResizeToFill
|
|||
from .locale import *
|
||||
from .profiles import *
|
||||
from .search import *
|
||||
from .. import location
|
||||
|
||||
import re
|
||||
import pytz
|
||||
|
@ -22,14 +24,13 @@ import hashlib
|
|||
|
||||
SLUG_OK = '-_~'
|
||||
|
||||
|
||||
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=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False)
|
||||
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)
|
||||
|
||||
|
@ -58,8 +59,8 @@ class Event(models.Model):
|
|||
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)
|
||||
|
||||
start_time = models.DateTimeField(help_text=_('Local date and time that the event starts'), verbose_name=_('Local Start Time'), db_index=True)
|
||||
end_time = models.DateTimeField(help_text=_('Local date and time that the event ends'), verbose_name=_('Local End Time'), db_index=True)
|
||||
start_time = models.DateTimeField(help_text=_('Date and time that the event starts'), verbose_name=_('Start Time'), db_index=True)
|
||||
end_time = models.DateTimeField(help_text=_('Date and 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)
|
||||
|
@ -68,7 +69,7 @@ class Event(models.Model):
|
|||
announce_url = models.URLField(verbose_name=_('Announcement'), help_text=_('URL for the announcement'), 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=datetime.datetime.now, db_index=True)
|
||||
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
|
||||
|
@ -76,6 +77,35 @@ class Event(models.Model):
|
|||
|
||||
attendees = models.ManyToManyField(UserProfile, through='Attendee', related_name="attending", blank=True)
|
||||
|
||||
@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
|
||||
|
||||
@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})
|
||||
|
||||
|
@ -112,7 +142,7 @@ def update_event_searchable(event):
|
|||
searchable = Searchable(event_uri)
|
||||
searchable.origin_node = origin_url
|
||||
searchable.federation_node = origin_url
|
||||
searchable.federation_time = datetime.datetime.now()
|
||||
searchable.federation_time = timezone.now()
|
||||
|
||||
searchable.event_url = event_url
|
||||
|
||||
|
@ -124,6 +154,7 @@ def update_event_searchable(event):
|
|||
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):
|
||||
|
@ -176,7 +207,7 @@ class Attendee(models.Model):
|
|||
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)
|
||||
joined_date = models.DateTimeField(default=datetime.datetime.now)
|
||||
joined_date = models.DateTimeField(default=timezone.now)
|
||||
|
||||
@property
|
||||
def role_name(self):
|
||||
|
@ -212,7 +243,7 @@ class EventComment(MPTTModel):
|
|||
author = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
|
||||
event = models.ForeignKey(Event, related_name='comments', on_delete=models.CASCADE)
|
||||
body = models.TextField()
|
||||
created_time = models.DateTimeField(default=datetime.datetime.now, db_index=True)
|
||||
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)
|
||||
|
||||
|
@ -224,8 +255,8 @@ class CommonEvent(models.Model):
|
|||
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(help_text=_('Local date and time that the event starts'), verbose_name=_('Local Start Time'), db_index=True)
|
||||
end_time = models.DateTimeField(help_text=_('Local date and time that the event ends'), verbose_name=_('Local End Time'), db_index=True)
|
||||
start_time = models.DateTimeField(help_text=_('Date and time that the event starts'), verbose_name=_('Start Time'), db_index=True)
|
||||
end_time = models.DateTimeField(help_text=_('Date and 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)
|
||||
|
||||
country = models.ForeignKey(Country, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
@ -237,7 +268,7 @@ class CommonEvent(models.Model):
|
|||
announce_url = models.URLField(verbose_name=_('Announcement'), help_text=_('URL for the announcement'), 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=datetime.datetime.now, db_index=True)
|
||||
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)
|
||||
|
|
|
@ -8,7 +8,9 @@ from imagekit.models import ProcessedImageField
|
|||
from imagekit.processors import ResizeToFill
|
||||
|
||||
from .locale import *
|
||||
from .. import location
|
||||
|
||||
import uuid
|
||||
import pytz
|
||||
import datetime
|
||||
import hashlib
|
||||
|
@ -18,7 +20,7 @@ class UserProfile(models.Model):
|
|||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
realname = models.CharField(verbose_name=_("Real Name"), max_length=150, blank=True)
|
||||
tz = models.CharField(max_length=32, verbose_name=_('Timezone'), default='UTC', choices=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False)
|
||||
tz = models.CharField(max_length=32, verbose_name=_('Timezone'), default='UTC', choices=location.TimezoneChoices(), blank=False, null=False)
|
||||
avatar = ProcessedImageField(verbose_name=_("Photo Image"),
|
||||
upload_to='avatars',
|
||||
processors=[ResizeToFill(128, 128)],
|
||||
|
@ -31,6 +33,8 @@ class UserProfile(models.Model):
|
|||
|
||||
send_notifications = models.BooleanField(verbose_name=_('Send notification emails'), default=True)
|
||||
|
||||
secret_key = models.UUIDField(default=uuid.uuid4, editable=True)
|
||||
|
||||
categories = models.ManyToManyField('Category', blank=True)
|
||||
topics = models.ManyToManyField('Topic', blank=True)
|
||||
|
||||
|
@ -196,7 +200,7 @@ class Team(models.Model):
|
|||
cover_img = models.URLField(_("Team Photo"), null=True, blank=True)
|
||||
languages = models.ManyToManyField(Language, blank=True)
|
||||
active = models.BooleanField(_("Active Team"), default=True)
|
||||
tz = models.CharField(max_length=32, verbose_name=_('Default Timezone'), default='UTC', choices=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False, help_text=_('The most commonly used timezone for this Team.'))
|
||||
tz = models.CharField(max_length=32, verbose_name=_('Default Timezone'), default='UTC', choices=location.TimezoneChoices(), blank=False, null=False, help_text=_('The most commonly used timezone for this Team.'))
|
||||
|
||||
members = models.ManyToManyField(UserProfile, through='Member', related_name="memberships", blank=True)
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .. import location
|
||||
|
||||
import pytz
|
||||
import datetime
|
||||
|
||||
# Provides a searchable index of events that may belong to this site or a federated site
|
||||
|
@ -17,6 +22,7 @@ class Searchable(models.Model):
|
|||
latitude = models.DecimalField(max_digits=12, decimal_places=8, null=True, blank=True)
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
tz = models.CharField(max_length=32, verbose_name=_('Default Timezone'), default='UTC', choices=location.TimezoneChoices(), blank=False, null=False, help_text=_('The most commonly used timezone for this Team.'))
|
||||
cost = models.PositiveSmallIntegerField(default=0, blank=True)
|
||||
tags = models.CharField(blank=True, null=True, max_length=128)
|
||||
|
||||
|
@ -27,6 +33,26 @@ class Searchable(models.Model):
|
|||
def __str__(self):
|
||||
return u'%s' % (self.event_url)
|
||||
|
||||
@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)
|
||||
|
||||
class SearchableSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Searchable
|
||||
|
|
|
@ -139,6 +139,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
USE_TZ = True
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
|
29
get_together/static/js/jquery-ui-lookup.js
vendored
29
get_together/static/js/jquery-ui-lookup.js
vendored
|
@ -28,6 +28,7 @@
|
|||
self.options.search(this.value, function(searchText, results){
|
||||
if (searchText != self.searchField[0].value) return ;
|
||||
|
||||
self.current_data = results;
|
||||
self.element.empty();
|
||||
var selected = " selected"
|
||||
self.element.append('<option value="">--------</option>')
|
||||
|
@ -43,7 +44,33 @@
|
|||
open: function(event) {
|
||||
this._super()
|
||||
this.searchField.focus()
|
||||
}
|
||||
},
|
||||
_select: function( item, event ) {
|
||||
var oldIndex = this.element[ 0 ].selectedIndex;
|
||||
|
||||
// Change native select element
|
||||
this.element[ 0 ].selectedIndex = item.index;
|
||||
this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) );
|
||||
this._setAria( item );
|
||||
|
||||
// Get lookup data for this item
|
||||
var data = this.current_data[item.index-1]
|
||||
|
||||
this._trigger( "select", event, { item: item, data: data } );
|
||||
|
||||
if ( item.index !== oldIndex ) {
|
||||
this._trigger( "change", event, { item: item } );
|
||||
}
|
||||
|
||||
this.close( event );
|
||||
},
|
||||
get_data_for: function(index) {
|
||||
if (this.current_data != null && current_data.length > index) {
|
||||
return this.current_data[index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
}(jQuery));
|
||||
|
|
|
@ -45,10 +45,10 @@
|
|||
|
||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item{% if request.resolver_match.url_name == "events" %} active{% endif %}">
|
||||
<li class="nav-item{% if request.resolver_match.url_name == "events" or request.resolver_match.url_name == "all-events" %} active{% endif %}">
|
||||
<a class="nav-link" href="{% url 'events' %}">Events{% if request.resolver_match.url_name == "events" %} <span class="sr-only">(current)</span>{% endif %}</a>
|
||||
</li>
|
||||
<li class="nav-item{% if request.resolver_match.url_name == "teams" %} active{% endif %}">
|
||||
<li class="nav-item{% if request.resolver_match.url_name == "teams" or request.resolver_match.url_name == "all-teams" %} active{% endif %}">
|
||||
<a class="nav-link" href="{% url 'teams' %}">Teams{% if request.resolver_match.url_name == "teams" %} <span class="sr-only">(current)</span>{% endif %}</a>
|
||||
</li>
|
||||
{% comment %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<form action="{% url "create-event" team.id%}" method="post">
|
||||
{% csrf_token %}
|
||||
{% if event.parent %}<<input type="hidden" name="common" value="{{event.parent.id}}" />{% endif %}
|
||||
{% if event.parent %}<input type="hidden" name="common" value="{{event.parent.id}}" />{% endif %}
|
||||
<div class="form-group">
|
||||
{% include "events/event_form.html" %}
|
||||
<br />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "get_together/base.html" %}
|
||||
{% load static %}
|
||||
{% load static tz %}
|
||||
|
||||
{% block styles %}
|
||||
<link href="{% static 'css/bootstrap-album.css' %}" rel="stylesheet"/>
|
||||
|
@ -29,7 +29,7 @@
|
|||
<div class="card-body">
|
||||
<p class="card-text"><strong><a class="card-link" href="{{event.get_absolute_url}}">{{event.name}}</a></strong></p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">{{ event.start_time }}</small>
|
||||
<small class="text-muted">{{ event.local_start_time }}</small>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-primary" href="{{ event.get_absolute_url }}">View</a></span>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "get_together/base.html" %}
|
||||
{% load markup static %}
|
||||
{% load markup static tz %}
|
||||
|
||||
{% block add_to_totle %} | {{event.name}}{% endblock %}
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
|||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><b>Time:</b></td><td>{{ event.start_time }} - {{ event.end_time }}</td>
|
||||
<td><b>Time:</b></td><td>{{ event.local_start_time }} - {{ event.local_end_time }}</td>
|
||||
</tr><tr>
|
||||
<td><b>Place:</b></td><td>
|
||||
{% if event.place %}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if near_events.count > 0 %}
|
||||
{% if near_events %}
|
||||
{% for event in near_events %}
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4 box-shadow">
|
||||
|
@ -37,7 +37,7 @@
|
|||
<div class="card-body">
|
||||
<p class="card-text"><strong><a class="card-link" href="{{event.event_url}}">{{event.event_title}}</a></strong></p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">{{ event.start_time }}<br/>{{event.location_name}}</small>
|
||||
<small class="text-muted">{{ event.local_start_time }}<br/>{{event.location_name}}</small>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-primary" href="{{event.event_url}}">View</a></span>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if near_teams.count > 0 %}
|
||||
{% if near_teams %}
|
||||
{% for team in near_teams %}
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4 box-shadow">
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
<div class="card-body">
|
||||
<p class="card-text"><strong><a class="card-link" href="{{event.get_absolute_url}}">{{event.name}}</a></strong></p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">{{ event.start_time }}</small>
|
||||
<small class="text-muted">{{ event.local_start_time }}</small>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-primary" href="{{ event.get_absolute_url }}">View</a></span>
|
||||
</div>
|
||||
|
|
|
@ -83,7 +83,8 @@
|
|||
selectField.append('<option value="'+ json.id +'" selected>'+ json.display + '</option>');
|
||||
selectField.lookup("refresh");
|
||||
|
||||
$("#id_tz").val(json.tz)
|
||||
$("#id_tz").val(json.tz);
|
||||
$("#id_tz").selectmenu("refresh");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -216,6 +217,10 @@ $(document).ready(function(){
|
|||
|
||||
return callback(q, data);
|
||||
});
|
||||
},
|
||||
select: function( event, ui ) {
|
||||
$("#id_tz").val(ui.data.tz);
|
||||
$("#id_tz").selectmenu("refresh");
|
||||
}
|
||||
})
|
||||
$("#id_tz").selectmenu();
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h6 class="mt-2 mb-0"><a href="{{event.get_absolute_url}}">{{event.name}}</a></h6>
|
||||
<small class="text-muted">{{ event.start_time }}</small>
|
||||
<small class="text-muted">{{ event.local_start_time }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -28,6 +28,10 @@ $(document).ready(function(){
|
|||
|
||||
return callback(q, data);
|
||||
});
|
||||
},
|
||||
select: function( event, ui ) {
|
||||
$("#id_tz").val(ui.data.tz);
|
||||
$("#id_tz").selectmenu("refresh");
|
||||
}
|
||||
})
|
||||
$("#id_category").selectmenu();
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
$(document).ready(function(){
|
||||
$("#city_select").lookup({
|
||||
search: function(searchText, callback) {
|
||||
if (searchText.length < 3) return callback(searchText, []);
|
||||
if (searchText.length < 3) return callback(searchText, []);
|
||||
$.getJSON("/api/cities/?q="+searchText, function(data) {
|
||||
var m = this.url.match(/q=([^&]+)/);
|
||||
var q = "";
|
||||
|
@ -27,8 +27,12 @@ $(document).ready(function(){
|
|||
|
||||
return callback(q, data);
|
||||
});
|
||||
},
|
||||
select: function( event, ui ) {
|
||||
$("#id_tz").val(ui.data.tz);
|
||||
$("#id_tz").selectmenu("refresh");
|
||||
}
|
||||
})
|
||||
});
|
||||
$("#id_category").selectmenu();
|
||||
$("#id_tz").selectmenu();
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "get_together/base.html" %}
|
||||
{% load markup %}
|
||||
{% load markup tz %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
|
@ -28,6 +28,7 @@
|
|||
{% else %}
|
||||
<a href="{% url 'join-team' team.id %}" class="btn btn-success btn-sm">Join Team</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'team-event-ical' team.id %}" class="btn btn-success btn-sm">iCal</a>
|
||||
</h2><hr/>
|
||||
|
||||
{% if team.description %}<p>{{ team.description|markdown }}</p><hr/>{% endif %}
|
||||
|
@ -38,7 +39,7 @@
|
|||
<div class="row">
|
||||
<div class="col"><a href="{{ event.get_absolute_url }}">{{event.name}}</a></div>
|
||||
<div class="col">{{ event.place }}</div>
|
||||
<div class="col">{{ event.start_time }}</div>
|
||||
<div class="col">{{ event.local_start_time }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if can_create_event %}
|
||||
|
@ -58,7 +59,7 @@
|
|||
<div class="row">
|
||||
<div class="col"><a href="{{ event.get_absolute_url }}">{{event.name}}</a></div>
|
||||
<div class="col">{{ event.place }}</div>
|
||||
<div class="col">{{ event.start_time }}</div>
|
||||
<div class="col">{{ event.local_start_time }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<img class="align-bottom" border="1" src="{{user.avatar_url}}" height="128px"/> {{user.user}}
|
||||
{% if user.user.id == request.user.id %}
|
||||
<a href="{% url 'edit-profile' %}" class="btn btn-secondary btn-sm">Edit Profile</a>
|
||||
<a href="{% url 'user-event-ical' request.user.profile.secret_key %}" class="btn btn-success btn-sm">iCal</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.conf import settings
|
|||
from django.conf.urls.static import static
|
||||
|
||||
from events import views as event_views
|
||||
from events import feeds
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -46,6 +47,7 @@ urlpatterns = [
|
|||
path('profile/+confirm_email/<str:confirmation_key>', views.user_confirm_email, name='confirm-email'),
|
||||
path('profile/+confirm_notifications', views.user_confirm_notifications, name='confirm-notifications'),
|
||||
path('profile/<int:user_id>/', views.show_profile, name='show-profile'),
|
||||
path('profile/<str:account_secret>.ics', feeds.UserEventsCalendar(), name='user-event-ical'),
|
||||
|
||||
path('events/', views.events_list, name='events'),
|
||||
path('events/all/', views.events_list_all, name='all-events'),
|
||||
|
@ -56,6 +58,7 @@ urlpatterns = [
|
|||
path('team/<int:team_id>/+join/', event_views.join_team, name='join-team'),
|
||||
path('team/<int:team_id>/+leave/', event_views.leave_team, name='leave-team'),
|
||||
path('team/<int:team_id>/+delete/', views.delete_team, name='delete-team'),
|
||||
path('team/<int:team_id>/events.ics', feeds.TeamEventsCalendar(), name='team-event-ical'),
|
||||
|
||||
path('+create-team/', views.create_team, name='create-team'),
|
||||
path('team/+create-event/', views.create_event_team_select, name='create-event-team-select'),
|
||||
|
|
|
@ -10,6 +10,7 @@ from events.models.events import Event, Place, Attendee
|
|||
from events.models.profiles import Team, UserProfile, Member
|
||||
from events.models.search import Searchable
|
||||
from events.forms import SearchForm
|
||||
from events import location
|
||||
|
||||
from accounts.decorators import setup_wanted
|
||||
from django.conf import settings
|
||||
|
@ -69,7 +70,7 @@ def home(request, *args, **kwards):
|
|||
if len(nearby_cities) == 0:
|
||||
city_distance += 1
|
||||
else:
|
||||
city = nearby_cities[0]
|
||||
city = sorted(nearby_cities, key=lambda city: location.city_distance_from(ll, city))[0]
|
||||
except:
|
||||
pass # City lookup failed
|
||||
|
||||
|
@ -93,10 +94,10 @@ def home(request, *args, **kwards):
|
|||
context['maxlng'] = maxlng
|
||||
|
||||
near_events = Searchable.objects.filter(latitude__gte=minlat, latitude__lte=maxlat, longitude__gte=minlng, longitude__lte=maxlng, end_time__gte=datetime.datetime.now())
|
||||
context['near_events'] = near_events
|
||||
context['near_events'] = sorted(near_events, key=lambda searchable: location.searchable_distance_from(ll, searchable))
|
||||
|
||||
near_teams = Team.objects.filter(city__latitude__gte=minlat, city__latitude__lte=maxlat, city__longitude__gte=minlng, city__longitude__lte=maxlng)
|
||||
context['near_teams'] = near_teams
|
||||
context['near_teams'] = sorted(near_teams, key=lambda team: location.team_distance_from(ll, team))
|
||||
except Exception as err:
|
||||
print("Error looking up nearby teams and events", err)
|
||||
traceback.print_exc()
|
||||
|
|
|
@ -5,11 +5,12 @@ from django.contrib.auth import logout as logout_user
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render, redirect, reverse
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
|
||||
from events.models.profiles import Team, Organization, UserProfile, Member
|
||||
from events.forms import TeamEventForm, NewTeamEventForm, DeleteEventForm, EventCommentForm, NewPlaceForm, UploadEventPhotoForm, NewCommonEventForm
|
||||
from django.utils import timezone
|
||||
|
||||
from events.models.events import Event, CommonEvent, EventPhoto, Place, Attendee
|
||||
from events.models.profiles import Team, Organization, UserProfile, Member
|
||||
from events.forms import TeamEventForm, NewTeamEventForm, DeleteEventForm, EventCommentForm, NewPlaceForm, UploadEventPhotoForm, NewCommonEventForm
|
||||
from events import location
|
||||
|
||||
import datetime
|
||||
import simplejson
|
||||
|
@ -18,18 +19,20 @@ import simplejson
|
|||
def events_list(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return redirect('all-events')
|
||||
events = Event.objects.filter(attendees=request.user.profile, end_time__gt=datetime.datetime.now()).order_by('start_time')
|
||||
events = Event.objects.filter(attendees=request.user.profile, end_time__gt=timezone.now()).order_by('start_time')
|
||||
geo_ip = location.get_geoip(request)
|
||||
context = {
|
||||
'active': 'my',
|
||||
'events_list': events,
|
||||
'events_list': sorted(events, key=lambda event: location.event_distance_from(geo_ip.latlng, event)),
|
||||
}
|
||||
return render(request, 'get_together/events/list_events.html', context)
|
||||
|
||||
def events_list_all(request, *args, **kwargs):
|
||||
events = Event.objects.filter(end_time__gt=datetime.datetime.now()).order_by('start_time')
|
||||
events = Event.objects.filter(end_time__gt=timezone.now()).order_by('start_time')
|
||||
geo_ip = location.get_geoip(request)
|
||||
context = {
|
||||
'active': 'all',
|
||||
'events_list': events,
|
||||
'events_list': sorted(events, key=lambda event: location.event_distance_from(geo_ip.latlng, event)),
|
||||
}
|
||||
return render(request, 'get_together/events/list_events.html', context)
|
||||
|
||||
|
@ -81,6 +84,7 @@ def create_event(request, team_id):
|
|||
form = NewTeamEventForm(request.POST, instance=new_event)
|
||||
if form.is_valid:
|
||||
new_event = form.save()
|
||||
Attendee.objects.create(event=new_event, user=request.user.profile, role=Attendee.HOST, status=Attendee.YES)
|
||||
return redirect('add-place', new_event.id)
|
||||
else:
|
||||
context = {
|
||||
|
|
|
@ -7,9 +7,9 @@ from django.shortcuts import render, redirect
|
|||
from django.http import HttpResponse, JsonResponse
|
||||
|
||||
from events.models.profiles import Organization, Team, UserProfile, Member
|
||||
from events.forms import TeamForm, NewTeamForm, DeleteTeamForm
|
||||
|
||||
from events.models.events import Event, CommonEvent, Place, Attendee
|
||||
from events.forms import TeamForm, NewTeamForm, DeleteTeamForm
|
||||
from events import location
|
||||
|
||||
import datetime
|
||||
import simplejson
|
||||
|
@ -19,18 +19,20 @@ def teams_list(request, *args, **kwargs):
|
|||
if not request.user.is_authenticated:
|
||||
return redirect('all-teams')
|
||||
|
||||
teams = request.user.profile.memberships.all()
|
||||
teams = request.user.profile.memberships.all().distinct()
|
||||
geo_ip = location.get_geoip(request)
|
||||
context = {
|
||||
'active': 'my',
|
||||
'teams': teams,
|
||||
'teams': sorted(teams, key=lambda team: location.team_distance_from(geo_ip.latlng, team)),
|
||||
}
|
||||
return render(request, 'get_together/teams/list_teams.html', context)
|
||||
|
||||
def teams_list_all(request, *args, **kwargs):
|
||||
teams = Team.objects.all()
|
||||
geo_ip = location.get_geoip(request)
|
||||
context = {
|
||||
'active': 'all',
|
||||
'teams': teams,
|
||||
'teams': sorted(teams, key=lambda team: location.team_distance_from(geo_ip.latlng, team)),
|
||||
}
|
||||
return render(request, 'get_together/teams/list_teams.html', context)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ defusedxml==0.5.0
|
|||
dj-database-url==0.4.2
|
||||
Django==2.0
|
||||
django-appconf==1.0.2
|
||||
django-ical==1.4
|
||||
django-imagekit==4.0.2
|
||||
django-imagekit-cropper==1.16
|
||||
django-js-asset==1.0.0
|
||||
|
@ -14,6 +15,7 @@ django-settings-export==1.2.1
|
|||
djangorestframework==3.7.7
|
||||
future==0.16.0
|
||||
geocoder==1.36.0
|
||||
icalendar==4.0.1
|
||||
idna==2.6
|
||||
Markdown==2.6.11
|
||||
model-mommy==1.5.1
|
||||
|
@ -21,6 +23,7 @@ oauthlib==2.0.6
|
|||
pilkit==2.0
|
||||
Pillow==5.0.0
|
||||
PyJWT==1.5.3
|
||||
python-dateutil==2.7.2
|
||||
python3-openid==3.1.0
|
||||
pytz==2017.3
|
||||
ratelim==0.1.6
|
||||
|
|
Loading…
Reference in a new issue