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:
Michael Hall 2018-04-01 22:22:30 -04:00
parent 2e21c5789e
commit d440e5b173
29 changed files with 493 additions and 47 deletions

66
events/feeds.py Normal file
View 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')

View file

@ -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
View 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

View 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)

View 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'),
),
]

View 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),
]

File diff suppressed because one or more lines are too long

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -139,6 +139,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us'
USE_TZ = True
TIME_ZONE = 'UTC'
USE_I18N = True

View file

@ -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));

View file

@ -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 %}

View file

@ -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 />

View file

@ -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>

View file

@ -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 %}

View file

@ -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">

View file

@ -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>

View file

@ -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();

View file

@ -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 %}

View file

@ -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();

View file

@ -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();
});

View file

@ -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>

View file

@ -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>

View file

@ -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'),

View file

@ -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()

View file

@ -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 = {

View file

@ -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)

View file

@ -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