From e84ae39d7d57b5663804226f0ecc3fb968bd3e6b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 31 Mar 2015 03:11:00 +0100 Subject: [PATCH 001/217] Added favicon and smartphone icon meta tags to template --- templates/base.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/base.html b/templates/base.html index 496d372b..aacf2a90 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,6 +11,10 @@ + + + + {% block css %} {% endblock %} From 80e8efc7636bcdc3854f24da81221a1ff85dd3af Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 31 Mar 2015 16:50:14 +0100 Subject: [PATCH 002/217] Changed to wavatar default --- RIGS/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/models.py b/RIGS/models.py index f31ff77d..a9b85f03 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -19,7 +19,7 @@ class Profile(AbstractUser): def profile_picture(self): url = "" if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None: - url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=identicon&s=500" + url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500" return url @property From 5bef0226c8b75c0e7d206044702725c9ceb8906e Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 3 Apr 2015 20:45:58 +0100 Subject: [PATCH 003/217] Attempt using ical extension - not working --- RIGS/urls.py | 3 ++ RIGS/views.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 90 insertions(+) diff --git a/RIGS/urls.py b/RIGS/urls.py index 27c932f9..22f0ac4e 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -114,6 +114,9 @@ urlpatterns = patterns('', url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()), name='profile_update_self'), + # ICS Calendar + url(r'^ics/$', (views.CalendarICS()), name="ics_calendar"), + # API url(r'^api/(?P\w+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"), url(r'^api/(?P\w+)/(?P\d+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"), diff --git a/RIGS/views.py b/RIGS/views.py index 57feffaa..56a302c2 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404 from django.core import serializers import simplejson from django.contrib import messages +from django_ical.views import ICalFeed from RIGS import models, forms @@ -263,6 +264,91 @@ class SecureAPIRequest(generic.View): return HttpResponse(model) +class CalendarICS(ICalFeed): + """ + A simple event calender + """ + product_id = '-//example.com//Example//EN' + timezone = 'UTC' + file_name = "event.ics" + + def items(self): + return models.Event.objects.all().order_by('-start_date') + + def item_title(self, item): + title = '' + if item.cancelled: + title += 'CANCELLED: ' + + if not item.is_rig: + title += 'NON-RIG: ' + + if item.dry_hire: + title += 'DRY HIRE: ' + + title += item.name + + title += ' ('+str(item.status)+')' + + return title + + def item_start_datetime(self, item): + startDateTime = item.start_date#.strftime("%Y%M%d") + + #if item.start_time: + # startDateTime += 'T'+item.start_time.strftime("%H%i") + + return startDateTime + + def item_end_datetime(self, item): + endDateTime = item.start_date.strftime("%Y%M%d") + + #if item.end_date: + # endDateTime = item.end_date.strftime("%Y%M%d") + # + #if item.start_time and item.end_time: # don't allow an event with specific end but no specific start + # endDateTime += 'T'+item.end_time.strftime("%H%i") + #elif item.start_time: # if there's a start time specified then an end time should also be specified + # endDateTime += 'T2359' + #elif item.end_time: # end time but no start time - this is weird - don't think ICS will like it so ignoring + # endDateTime += '' # do nothing + + return endDateTime + + def item_location(self,item): + return item.venue + + def item_description(self, item): + desc = 'Rig ID = '+str(item.pk)+'\n' + desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' + desc += 'Status = ' + str(item.status) + '\n' + desc += 'Event = ' + item.name + '\n' + desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' + if item.is_rig and item.person: + desc += 'Client = ' + item.person.name + ( ('for'+item.organisation.name) if item.organisation else '') + '\n' + desc += '\n\n' + if item.description: + desc += 'Event Description:\n'+item.description + if item.notes: + desc += 'Notes:\n'+item.notes + + + + + return item.description + + def item_link(self, item): + return '' + + # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) + # return '' + + def item_updated(self, item): + return item.last_edited_at + + def item_guid(self, item): + return item.pk + class ProfileDetail(generic.DetailView): model = models.Profile diff --git a/requirements.txt b/requirements.txt index a8c14984..2afebf22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pillow==2.7.0 reportlab==2.7 z3c.rml==2.7.2 pyPDF2==1.23 +django-ical==1.3 \ No newline at end of file From 980f8638161d0ddb5ae3c52b974b430165e9b1b5 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 4 Apr 2015 00:19:17 +0100 Subject: [PATCH 004/217] Working version of ics interface - not very efficient --- RIGS/urls.py | 2 +- RIGS/views.py | 74 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/RIGS/urls.py b/RIGS/urls.py index 22f0ac4e..c8f095e9 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -115,7 +115,7 @@ urlpatterns = patterns('', name='profile_update_self'), # ICS Calendar - url(r'^ics/$', (views.CalendarICS()), name="ics_calendar"), + url(r'^calendar/rigs.ics$', (views.CalendarICS()), name="ics_calendar"), # API url(r'^api/(?P\w+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"), diff --git a/RIGS/views.py b/RIGS/views.py index 56a302c2..1e0095f2 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -9,6 +9,7 @@ from django.core import serializers import simplejson from django.contrib import messages from django_ical.views import ICalFeed +import datetime from RIGS import models, forms @@ -268,12 +269,18 @@ class CalendarICS(ICalFeed): """ A simple event calender """ - product_id = '-//example.com//Example//EN' + product_id = 'PyRIGS' + title = 'PyRIGS Calendar' timezone = 'UTC' - file_name = "event.ics" + file_name = "rigs.ics" def items(self): - return models.Event.objects.all().order_by('-start_date') + #get events for today +- 1 year + start = datetime.datetime.now() - datetime.timedelta(days=1*365) + end = datetime.date.today() + datetime.timedelta(days=1*365) + filter = Q(start_date__lte=end) & Q(start_date__gte=start) + + return models.Event.objects.filter(filter).order_by('-start_date') def item_title(self, item): title = '' @@ -288,30 +295,35 @@ class CalendarICS(ICalFeed): title += item.name - title += ' ('+str(item.status)+')' + title += ' ('+str(item.get_status_display())+')' return title def item_start_datetime(self, item): - startDateTime = item.start_date#.strftime("%Y%M%d") - - #if item.start_time: - # startDateTime += 'T'+item.start_time.strftime("%H%i") + #set start date to the earliest defined time for the event + if item.meet_at: + startDateTime = item.meet_at + elif item.access_at: + startDateTime = item.access_at + elif item.start_time: + startDateTime = datetime.datetime.combine(item.start_date,item.start_time) + else: + startDateTime = item.start_date return startDateTime def item_end_datetime(self, item): - endDateTime = item.start_date.strftime("%Y%M%d") + endDateTime = item.start_date - #if item.end_date: - # endDateTime = item.end_date.strftime("%Y%M%d") - # - #if item.start_time and item.end_time: # don't allow an event with specific end but no specific start - # endDateTime += 'T'+item.end_time.strftime("%H%i") - #elif item.start_time: # if there's a start time specified then an end time should also be specified - # endDateTime += 'T2359' + if item.end_date: + endDateTime = item.end_date + + if item.start_time and item.end_time: # don't allow an event with specific end but no specific start + endDateTime = datetime.datetime.combine(endDateTime,item.end_time) + elif item.start_time: # if there's a start time specified then an end time should also be specified + endDateTime = datetime.datetime.combine(endDateTime+datetime.timedelta(days=1),datetime.time(00, 00)) #elif item.end_time: # end time but no start time - this is weird - don't think ICS will like it so ignoring - # endDateTime += '' # do nothing + # do nothing return endDateTime @@ -320,25 +332,35 @@ class CalendarICS(ICalFeed): def item_description(self, item): desc = 'Rig ID = '+str(item.pk)+'\n' - desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' - desc += 'Status = ' + str(item.status) + '\n' desc += 'Event = ' + item.name + '\n' desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' if item.is_rig and item.person: - desc += 'Client = ' + item.person.name + ( ('for'+item.organisation.name) if item.organisation else '') + '\n' - desc += '\n\n' + desc += 'Client = ' + item.person.name + ( (' for '+item.organisation.name) if item.organisation else '') + '\n' + desc += 'Status = ' + str(item.get_status_display()) + '\n' + desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' + + + desc += '\n' + if item.meet_at: + desc += 'Crew Meet = ' + item.meet_at.strftime('%Y-%m-%d %H:%M') + (('('+item.meet_info+')') if item.meet_info else '---') + '\n' + if item.access_at: + desc += 'Access At = ' + item.access_at.strftime('%Y-%m-%d %H:%M') + '\n' + if item.start_date: + desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.start_time else '') + '\n' + if item.end_date: + desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.end_time else '') + '\n' + + desc += '\n' if item.description: - desc += 'Event Description:\n'+item.description + desc += 'Event Description:\n'+item.description+'\n\n' if item.notes: desc += 'Notes:\n'+item.notes - - - return item.description + return desc def item_link(self, item): - return '' + return '/event/'+str(item.pk)+'/' # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) # return '' From f21ca248be177ed0fbde5ab78db9c370c7dd2eec Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 18 Apr 2015 22:39:58 +0100 Subject: [PATCH 005/217] Made more efficient & split view into a separate file --- RIGS/ical.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ RIGS/urls.py | 4 +- RIGS/views.py | 107 ------------------------------------------------ 3 files changed, 113 insertions(+), 109 deletions(-) create mode 100644 RIGS/ical.py diff --git a/RIGS/ical.py b/RIGS/ical.py new file mode 100644 index 00000000..728f8485 --- /dev/null +++ b/RIGS/ical.py @@ -0,0 +1,111 @@ +from RIGS import models, forms +from django_ical.views import ICalFeed +from django.db.models import Q + +import datetime + +class CalendarICS(ICalFeed): + """ + A simple event calender + """ + product_id = 'PyRIGS' + title = 'PyRIGS Calendar' + timezone = 'UTC' + file_name = "rigs.ics" + + def items(self): + #get events for today +- 1 year + start = datetime.datetime.now() - datetime.timedelta(days=31*3) + end = datetime.date.today() + datetime.timedelta(days=31*3) + filter = Q(start_date__gte=start) + + return models.Event.objects.filter(filter).select_related('person', 'organisation', 'venue', 'mic').order_by('-start_date') + + def item_title(self, item): + title = '' + if item.cancelled: + title += 'CANCELLED: ' + + if not item.is_rig: + title += 'NON-RIG: ' + + if item.dry_hire: + title += 'DRY HIRE: ' + + title += item.name + + title += ' ('+str(item.get_status_display())+')' + + return title + + def item_start_datetime(self, item): + #set start date to the earliest defined time for the event + if item.meet_at: + startDateTime = item.meet_at + elif item.access_at: + startDateTime = item.access_at + elif item.start_time: + startDateTime = datetime.datetime.combine(item.start_date,item.start_time) + else: + startDateTime = item.start_date + + return startDateTime + + def item_end_datetime(self, item): + endDateTime = item.start_date + + if item.end_date: + endDateTime = item.end_date + + if item.start_time and item.end_time: # don't allow an event with specific end but no specific start + endDateTime = datetime.datetime.combine(endDateTime,item.end_time) + elif item.start_time: # if there's a start time specified then an end time should also be specified + endDateTime = datetime.datetime.combine(endDateTime+datetime.timedelta(days=1),datetime.time(00, 00)) + #elif item.end_time: # end time but no start time - this is weird - don't think ICS will like it so ignoring + # do nothing + + return endDateTime + + def item_location(self,item): + return item.venue + + def item_description(self, item): + desc = 'Rig ID = '+str(item.pk)+'\n' + desc += 'Event = ' + item.name + '\n' + desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' + if item.is_rig and item.person: + desc += 'Client = ' + item.person.name + ( (' for '+item.organisation.name) if item.organisation else '') + '\n' + desc += 'Status = ' + str(item.get_status_display()) + '\n' + desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' + + + desc += '\n' + if item.meet_at: + desc += 'Crew Meet = ' + item.meet_at.strftime('%Y-%m-%d %H:%M') + (('('+item.meet_info+')') if item.meet_info else '---') + '\n' + if item.access_at: + desc += 'Access At = ' + item.access_at.strftime('%Y-%m-%d %H:%M') + '\n' + if item.start_date: + desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.start_time else '') + '\n' + if item.end_date: + desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.end_time else '') + '\n' + + desc += '\n' + if item.description: + desc += 'Event Description:\n'+item.description+'\n\n' + if item.notes: + desc += 'Notes:\n'+item.notes + + + return desc + + def item_link(self, item): + return '/event/'+str(item.pk)+'/' + + # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) + # return '' + + def item_updated(self, item): + return item.last_edited_at + + def item_guid(self, item): + return item.pk \ No newline at end of file diff --git a/RIGS/urls.py b/RIGS/urls.py index c8f095e9..f25dbfa6 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import patterns, include, url from django.contrib.auth.decorators import login_required -from RIGS import views, rigboard, finance +from RIGS import views, rigboard, finance, ical from django.views.generic import RedirectView from PyRIGS.decorators import permission_required_with_403 @@ -115,7 +115,7 @@ urlpatterns = patterns('', name='profile_update_self'), # ICS Calendar - url(r'^calendar/rigs.ics$', (views.CalendarICS()), name="ics_calendar"), + url(r'^calendar/rigs.ics$', (ical.CalendarICS()), name="ics_calendar"), # API url(r'^api/(?P\w+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"), diff --git a/RIGS/views.py b/RIGS/views.py index 1e0095f2..e1f78979 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -8,7 +8,6 @@ from django.shortcuts import get_object_or_404 from django.core import serializers import simplejson from django.contrib import messages -from django_ical.views import ICalFeed import datetime from RIGS import models, forms @@ -265,112 +264,6 @@ class SecureAPIRequest(generic.View): return HttpResponse(model) -class CalendarICS(ICalFeed): - """ - A simple event calender - """ - product_id = 'PyRIGS' - title = 'PyRIGS Calendar' - timezone = 'UTC' - file_name = "rigs.ics" - - def items(self): - #get events for today +- 1 year - start = datetime.datetime.now() - datetime.timedelta(days=1*365) - end = datetime.date.today() + datetime.timedelta(days=1*365) - filter = Q(start_date__lte=end) & Q(start_date__gte=start) - - return models.Event.objects.filter(filter).order_by('-start_date') - - def item_title(self, item): - title = '' - if item.cancelled: - title += 'CANCELLED: ' - - if not item.is_rig: - title += 'NON-RIG: ' - - if item.dry_hire: - title += 'DRY HIRE: ' - - title += item.name - - title += ' ('+str(item.get_status_display())+')' - - return title - - def item_start_datetime(self, item): - #set start date to the earliest defined time for the event - if item.meet_at: - startDateTime = item.meet_at - elif item.access_at: - startDateTime = item.access_at - elif item.start_time: - startDateTime = datetime.datetime.combine(item.start_date,item.start_time) - else: - startDateTime = item.start_date - - return startDateTime - - def item_end_datetime(self, item): - endDateTime = item.start_date - - if item.end_date: - endDateTime = item.end_date - - if item.start_time and item.end_time: # don't allow an event with specific end but no specific start - endDateTime = datetime.datetime.combine(endDateTime,item.end_time) - elif item.start_time: # if there's a start time specified then an end time should also be specified - endDateTime = datetime.datetime.combine(endDateTime+datetime.timedelta(days=1),datetime.time(00, 00)) - #elif item.end_time: # end time but no start time - this is weird - don't think ICS will like it so ignoring - # do nothing - - return endDateTime - - def item_location(self,item): - return item.venue - - def item_description(self, item): - desc = 'Rig ID = '+str(item.pk)+'\n' - desc += 'Event = ' + item.name + '\n' - desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' - if item.is_rig and item.person: - desc += 'Client = ' + item.person.name + ( (' for '+item.organisation.name) if item.organisation else '') + '\n' - desc += 'Status = ' + str(item.get_status_display()) + '\n' - desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' - - - desc += '\n' - if item.meet_at: - desc += 'Crew Meet = ' + item.meet_at.strftime('%Y-%m-%d %H:%M') + (('('+item.meet_info+')') if item.meet_info else '---') + '\n' - if item.access_at: - desc += 'Access At = ' + item.access_at.strftime('%Y-%m-%d %H:%M') + '\n' - if item.start_date: - desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.start_time else '') + '\n' - if item.end_date: - desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.end_time else '') + '\n' - - desc += '\n' - if item.description: - desc += 'Event Description:\n'+item.description+'\n\n' - if item.notes: - desc += 'Notes:\n'+item.notes - - - return desc - - def item_link(self, item): - return '/event/'+str(item.pk)+'/' - - # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - # return '' - - def item_updated(self, item): - return item.last_edited_at - - def item_guid(self, item): - return item.pk - class ProfileDetail(generic.DetailView): model = models.Profile From 94194d0613d68200d3de514ccaf7a1ae462b4e15 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 18 Apr 2015 23:12:57 +0100 Subject: [PATCH 006/217] Tidied up & added comments --- RIGS/ical.py | 24 +++++++++++++++++------- RIGS/urls.py | 4 ++-- RIGS/views.py | 1 - 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/RIGS/ical.py b/RIGS/ical.py index 728f8485..6f0c72f7 100644 --- a/RIGS/ical.py +++ b/RIGS/ical.py @@ -8,21 +8,23 @@ class CalendarICS(ICalFeed): """ A simple event calender """ + #Metadata which is passed on to clients product_id = 'PyRIGS' title = 'PyRIGS Calendar' timezone = 'UTC' file_name = "rigs.ics" def items(self): - #get events for today +- 1 year - start = datetime.datetime.now() - datetime.timedelta(days=31*3) - end = datetime.date.today() + datetime.timedelta(days=31*3) + #include events from up to 1 year ago + start = datetime.datetime.now() - datetime.timedelta(days=365) filter = Q(start_date__gte=start) - return models.Event.objects.filter(filter).select_related('person', 'organisation', 'venue', 'mic').order_by('-start_date') + return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') def item_title(self, item): title = '' + + # Prefix title with status (if it's a critical status) if item.cancelled: title += 'CANCELLED: ' @@ -32,8 +34,10 @@ class CalendarICS(ICalFeed): if item.dry_hire: title += 'DRY HIRE: ' + # Add the rig name title += item.name + # Add the status title += ' ('+str(item.get_status_display())+')' return title @@ -52,8 +56,10 @@ class CalendarICS(ICalFeed): return startDateTime def item_end_datetime(self, item): + # Assume end is same as start endDateTime = item.start_date + # If end date defined then use it if item.end_date: endDateTime = item.end_date @@ -70,6 +76,9 @@ class CalendarICS(ICalFeed): return item.venue def item_description(self, item): + # Create a nice information-rich description + # note: only making use of information available to "non-keyholders" + desc = 'Rig ID = '+str(item.pk)+'\n' desc += 'Event = ' + item.name + '\n' desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' @@ -99,13 +108,14 @@ class CalendarICS(ICalFeed): return desc def item_link(self, item): + # Make a link to the event in the web interface return '/event/'+str(item.pk)+'/' - # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) + # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though # return '' - def item_updated(self, item): + def item_updated(self, item): # some ical clients will display this return item.last_edited_at - def item_guid(self, item): + def item_guid(self, item): # use the rig-id as the ical unique event identifier return item.pk \ No newline at end of file diff --git a/RIGS/urls.py b/RIGS/urls.py index f25dbfa6..12cc015d 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -114,8 +114,8 @@ urlpatterns = patterns('', url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()), name='profile_update_self'), - # ICS Calendar - url(r'^calendar/rigs.ics$', (ical.CalendarICS()), name="ics_calendar"), + # ICS Calendar - no authentication! + url(r'^ical/rigs.ics$', (ical.CalendarICS()), name="ics_calendar"), # API url(r'^api/(?P\w+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"), diff --git a/RIGS/views.py b/RIGS/views.py index e1f78979..57feffaa 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -8,7 +8,6 @@ from django.shortcuts import get_object_or_404 from django.core import serializers import simplejson from django.contrib import messages -import datetime from RIGS import models, forms From df88222e00ea312e18968077030cf96a9eb049ef Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 17:06:03 +0100 Subject: [PATCH 007/217] Added api key stuff to profile model --- RIGS/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/RIGS/models.py b/RIGS/models.py index f31ff77d..79736131 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -7,6 +7,8 @@ from django.conf import settings from django.utils.functional import cached_property from django.utils.encoding import python_2_unicode_compatible import reversion +import string +import random from decimal import Decimal @@ -14,6 +16,14 @@ from decimal import Decimal class Profile(AbstractUser): initials = models.CharField(max_length=5, unique=True, null=True, blank=False) phone = models.CharField(max_length=13, null=True, blank=True) + api_key = models.CharField(max_length=40,blank=False,editable=True,default=lambda: make_api_key()) + + @classmethod + def make_api_key(cls): + size=20 + chars=string.ascii_letters + string.digits + new_api_key = ''.join(random.choice(chars) for x in range(size)) + return new_api_key; @property def profile_picture(self): @@ -26,7 +36,6 @@ class Profile(AbstractUser): def name(self): return self.get_full_name() + ' "' + self.initials + '"' - class RevisionMixin(object): @property def last_edited_at(self): From 29f873c64082b2db87122e879e5662da2aa9f7dd Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 17:06:52 +0100 Subject: [PATCH 008/217] Add interface to view API key --- RIGS/templates/RIGS/profile_detail.html | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/RIGS/templates/RIGS/profile_detail.html b/RIGS/templates/RIGS/profile_detail.html index a9d9bbaf..56a81fba 100644 --- a/RIGS/templates/RIGS/profile_detail.html +++ b/RIGS/templates/RIGS/profile_detail.html @@ -16,7 +16,7 @@ {% endif %} -
+
First Name
{{object.first_name}}
@@ -39,10 +39,30 @@
Phone
{{object.phone}}
+ {% if object.pk == user.pk %} + + + +

Personal iCal Details

+ +
+
API Key
+
{{user.api_key}}
+ +
Calendar URL
+
https://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}
+
+ {% endif %}
-
+ +
+
{% endblock %} \ No newline at end of file From a7f392f0bcdde9cc3685849f3ffc08dd700d5cd1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 17:07:28 +0100 Subject: [PATCH 009/217] Added authentication decorator & reset key functionality --- RIGS/urls.py | 6 ++++-- RIGS/views.py | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/RIGS/urls.py b/RIGS/urls.py index 12cc015d..3b3937b4 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -4,6 +4,7 @@ from RIGS import views, rigboard, finance, ical from django.views.generic import RedirectView from PyRIGS.decorators import permission_required_with_403 +from PyRIGS.decorators import api_key_required urlpatterns = patterns('', # Examples: @@ -113,9 +114,10 @@ urlpatterns = patterns('', name='profile_detail'), url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()), name='profile_update_self'), + url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'), - # ICS Calendar - no authentication! - url(r'^ical/rigs.ics$', (ical.CalendarICS()), name="ics_calendar"), + # ICS Calendar - API key authentication + url(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"), # API url(r'^api/(?P\w+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"), diff --git a/RIGS/views.py b/RIGS/views.py index 57feffaa..dd4176a2 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -287,4 +287,12 @@ class ProfileUpdateSelf(generic.UpdateView): def get_success_url(self): url = reverse_lazy('profile_detail') - return url \ No newline at end of file + return url + +class ResetApiKey(generic.RedirectView): + def get_redirect_url(self, *args, **kwargs): + self.request.user.api_key = self.request.user.make_api_key() + + self.request.user.save() + + return reverse_lazy('profile_detail') \ No newline at end of file From a7a1d4d3b8c2842a153784b2b86195370e79fada Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 17:08:16 +0100 Subject: [PATCH 010/217] Actually added authentication decorator this time --- PyRIGS/decorators.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py index 5e4e613d..a7c1db90 100644 --- a/PyRIGS/decorators.py +++ b/PyRIGS/decorators.py @@ -33,4 +33,35 @@ def permission_required_with_403(perm, login_url=None): Decorator for views that checks whether a user has a particular permission enabled, redirecting to the log-in page or rendering a 403 as necessary. """ - return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url) \ No newline at end of file + return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url) + +from RIGS import models + +def api_key_required(function): + """ + Decorator for views that checks api_pk and api_key. + Failed users will be given a 403 error. + Should only be used for urls which include and kwargs + """ + def wrap(request, *args, **kwargs): + + userid = kwargs.get('api_pk') + key = kwargs.get('api_key') + + error_resp = render_to_response('403.html', context_instance=RequestContext(request)) + error_resp.status_code = 403 + + if key is None: + return error_resp + if userid is None: + return error_resp + + try: + user_object = models.Profile.objects.get(pk=userid) + except Profile.DoesNotExist: + return error_resp + + if user_object.api_key != key: + return error_resp + return function(request, *args, **kwargs) + return wrap \ No newline at end of file From f40cfa749050cf17a3eb77daaad70adfa07ce1f3 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 17:28:22 +0100 Subject: [PATCH 011/217] Made it work for users that already exist in database --- RIGS/models.py | 4 ++-- RIGS/templates/RIGS/profile_detail.html | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/RIGS/models.py b/RIGS/models.py index 79736131..09a70d51 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -16,8 +16,8 @@ from decimal import Decimal class Profile(AbstractUser): initials = models.CharField(max_length=5, unique=True, null=True, blank=False) phone = models.CharField(max_length=13, null=True, blank=True) - api_key = models.CharField(max_length=40,blank=False,editable=True,default=lambda: make_api_key()) - + api_key = models.CharField(max_length=40,blank=True,editable=False, null=True) + @classmethod def make_api_key(cls): size=20 diff --git a/RIGS/templates/RIGS/profile_detail.html b/RIGS/templates/RIGS/profile_detail.html index 56a81fba..1e7174f6 100644 --- a/RIGS/templates/RIGS/profile_detail.html +++ b/RIGS/templates/RIGS/profile_detail.html @@ -43,7 +43,8 @@ @@ -51,11 +52,24 @@
API Key
-
{{user.api_key}}
+
+ {% if user.api_key %} + {{user.api_key}} + {% else %} + No API Key Generated + {% endif %} +
Calendar URL
-
https://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}
+
+ {% if user.api_key %} +
https://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}
+ {% else %} +
No API Key Generated
+ {% endif %} +
+ {% endif %}
From df1a285089b5851cbc9c247acf1e1d0a2ae2e61a Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 22:25:59 +0100 Subject: [PATCH 012/217] Added calendar view --- RIGS/rigboard.py | 2 + RIGS/static/css/fullcalendar.css | 1061 +++ RIGS/static/css/fullcalendar.print.css | 202 + RIGS/static/js/fullcalendar.js | 10789 +++++++++++++++++++++++ RIGS/static/js/moment.min.js | 7 + RIGS/templates/RIGS/calendar.html | 111 + RIGS/templates/RIGS/event_detail.html | 50 +- RIGS/urls.py | 4 +- RIGS/views.py | 68 + templates/base.html | 1 + 10 files changed, 12284 insertions(+), 11 deletions(-) create mode 100755 RIGS/static/css/fullcalendar.css create mode 100755 RIGS/static/css/fullcalendar.print.css create mode 100755 RIGS/static/js/fullcalendar.js create mode 100755 RIGS/static/js/moment.min.js create mode 100644 RIGS/templates/RIGS/calendar.html diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py index 3895b16f..3fe891fe 100644 --- a/RIGS/rigboard.py +++ b/RIGS/rigboard.py @@ -33,6 +33,8 @@ class RigboardIndex(generic.TemplateView): context['events'] = models.Event.objects.current_events() return context +class WebCalendar(generic.TemplateView): + template_name = 'RIGS/calendar.html' class EventDetail(generic.DetailView): model = models.Event diff --git a/RIGS/static/css/fullcalendar.css b/RIGS/static/css/fullcalendar.css new file mode 100755 index 00000000..ba09da6b --- /dev/null +++ b/RIGS/static/css/fullcalendar.css @@ -0,0 +1,1061 @@ +/*! + * FullCalendar v2.3.1 Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw + */ + + +.fc { + direction: ltr; + text-align: left; +} + +.fc-rtl { + text-align: right; +} + +body .fc { /* extra precedence to overcome jqui */ + font-size: 1em; +} + + +/* Colors +--------------------------------------------------------------------------------------------------*/ + +.fc-unthemed th, +.fc-unthemed td, +.fc-unthemed thead, +.fc-unthemed tbody, +.fc-unthemed .fc-divider, +.fc-unthemed .fc-row, +.fc-unthemed .fc-popover { + border-color: #ddd; +} + +.fc-unthemed .fc-popover { + background-color: #fff; +} + +.fc-unthemed .fc-divider, +.fc-unthemed .fc-popover .fc-header { + background: #eee; +} + +.fc-unthemed .fc-popover .fc-header .fc-close { + color: #666; +} + +.fc-unthemed .fc-today { + background: #fcf8e3; +} + +.fc-highlight { /* when user is selecting cells */ + background: #bce8f1; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ +} + +.fc-bgevent { /* default look for background events */ + background: rgb(143, 223, 130); + opacity: .3; + filter: alpha(opacity=30); /* for IE */ +} + +.fc-nonbusiness { /* default look for non-business-hours areas */ + /* will inherit .fc-bgevent's styles */ + background: #d7d7d7; +} + + +/* Icons (inline elements with styled text that mock arrow icons) +--------------------------------------------------------------------------------------------------*/ + +.fc-icon { + display: inline-block; + width: 1em; + height: 1em; + line-height: 1em; + font-size: 1em; + text-align: center; + overflow: hidden; + font-family: "Courier New", Courier, monospace; +} + +/* +Acceptable font-family overrides for individual icons: + "Arial", sans-serif + "Times New Roman", serif + +NOTE: use percentage font sizes or else old IE chokes +*/ + +.fc-icon:after { + position: relative; + margin: 0 -1em; /* ensures character will be centered, regardless of width */ +} + +.fc-icon-left-single-arrow:after { + content: "\02039"; + font-weight: bold; + font-size: 200%; + top: -7%; + left: 3%; +} + +.fc-icon-right-single-arrow:after { + content: "\0203A"; + font-weight: bold; + font-size: 200%; + top: -7%; + left: -3%; +} + +.fc-icon-left-double-arrow:after { + content: "\000AB"; + font-size: 160%; + top: -7%; +} + +.fc-icon-right-double-arrow:after { + content: "\000BB"; + font-size: 160%; + top: -7%; +} + +.fc-icon-left-triangle:after { + content: "\25C4"; + font-size: 125%; + top: 3%; + left: -2%; +} + +.fc-icon-right-triangle:after { + content: "\25BA"; + font-size: 125%; + top: 3%; + left: 2%; +} + +.fc-icon-down-triangle:after { + content: "\25BC"; + font-size: 125%; + top: 2%; +} + +.fc-icon-x:after { + content: "\000D7"; + font-size: 200%; + top: 6%; +} + + +/* Buttons (styled ' + ) + .click(function() { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { + + buttonClick(); + + // after the click action, if the button becomes the "active" tab, or disabled, + // it should never have a hover class, so remove it now. + if ( + button.hasClass(tm + '-state-active') || + button.hasClass(tm + '-state-disabled') + ) { + button.removeClass(tm + '-state-hover'); + } + } + }) + .mousedown(function() { + // the *down* effect (mouse pressed in). + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + // undo the *down* effect + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + // the *hover* effect. + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + // undo the *hover* effect + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup + } + ); + + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); + } + + if (groupChildren.length > 1) { + groupEl = $('
'); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } + + return sectionEl; + } + + + function updateTitle(text) { + el.find('h2').text(text); + } + + + function activateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .attr('disabled', 'disabled') + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeAttr('disabled') + .removeClass(tm + '-state-disabled'); + } + + + function getViewsWithButtons() { + return viewsWithButtons; + } + +} + +;; + +fc.sourceNormalizers = []; +fc.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager(options) { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + t.normalizeEventRange = normalizeEventRange; + t.normalizeEventRangeTimes = normalizeEventRangeTimes; + t.ensureVisibleEventRange = ensureVisibleEventRange; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; // holds events that have already been expanded + + + $.each( + (options.events ? [ options.events ] : []).concat(options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + // or, a part of the new range is outside of the old range? (after normalizing) + start.clone().stripZone() < rangeStart.clone().stripZone() || + end.clone().stripZone() > rangeEnd.clone().stripZone(); + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i eventRange system + function ensureVisibleEventRange(range) { + var allDay; + + if (!range.end) { + + allDay = range.allDay; // range might be more event-ish than we think + if (allDay == null) { + allDay = !range.start.hasTime(); + } + + range = $.extend({}, range); // make a copy, copying over other misc properties + range.end = t.getDefaultEventEnd(allDay, range.start); + } + return range; + } + + + // If the given event is a recurring event, break it down into an array of individual instances. + // If not a recurring event, return an array with the single original event. + // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array. + // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours). + function expandEvent(abstractEvent, _rangeStart, _rangeEnd) { + var events = []; + var dowHash; + var dow; + var i; + var date; + var startTime, endTime; + var start, end; + var event; + + _rangeStart = _rangeStart || rangeStart; + _rangeEnd = _rangeEnd || rangeEnd; + + if (abstractEvent) { + if (abstractEvent._recurring) { + + // make a boolean hash as to whether the event occurs on each day-of-week + if ((dow = abstractEvent.dow)) { + dowHash = {}; + for (i = 0; i < dow.length; i++) { + dowHash[dow[i]] = true; + } + } + + // iterate through every day in the current range + date = _rangeStart.clone().stripTime(); // holds the date of the current day + while (date.isBefore(_rangeEnd)) { + + if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week + + startTime = abstractEvent.start; // the stored start and end properties are times (Durations) + endTime = abstractEvent.end; // " + start = date.clone(); + end = null; + + if (startTime) { + start = start.time(startTime); + } + if (endTime) { + end = date.clone().time(endTime); + } + + event = $.extend({}, abstractEvent); // make a copy of the original + assignDatesToEvent( + start, end, + !startTime && !endTime, // allDay? + event + ); + events.push(event); + } + + date.add(1, 'days'); + } + } + else { + events.push(abstractEvent); // return the original event. will be a one-item array + } + } + + return events; + } + + + + /* Event Modification Math + -----------------------------------------------------------------------------------------*/ + + + // Modifies an event and all related events by applying the given properties. + // Special date-diffing logic is used for manipulation of dates. + // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end. + // All date comparisons are done against the event's pristine _start and _end dates. + // Returns an object with delta information and a function to undo all operations. + // For making computations in a granularity greater than day/time, specify largeUnit. + // NOTE: The given `newProps` might be mutated for normalization purposes. + function mutateEvent(event, newProps, largeUnit) { + var miscProps = {}; + var oldProps; + var clearEnd; + var startDelta; + var endDelta; + var durationDelta; + var undoFunc; + + // diffs the dates in the appropriate way, returning a duration + function diffDates(date1, date0) { // date1 - date0 + if (largeUnit) { + return diffByUnit(date1, date0, largeUnit); + } + else if (newProps.allDay) { + return diffDay(date1, date0); + } + else { + return diffDayTime(date1, date0); + } + } + + newProps = newProps || {}; + + // normalize new date-related properties + if (!newProps.start) { + newProps.start = event.start.clone(); + } + if (newProps.end === undefined) { + newProps.end = event.end ? event.end.clone() : null; + } + if (newProps.allDay == null) { // is null or undefined? + newProps.allDay = event.allDay; + } + normalizeEventRange(newProps); + + // create normalized versions of the original props to compare against + // need a real end value, for diffing + oldProps = { + start: event._start.clone(), + end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), + allDay: newProps.allDay // normalize the dates in the same regard as the new properties + }; + normalizeEventRange(oldProps); + + // need to clear the end date if explicitly changed to null + clearEnd = event._end !== null && newProps.end === null; + + // compute the delta for moving the start date + startDelta = diffDates(newProps.start, oldProps.start); + + // compute the delta for moving the end date + if (newProps.end) { + endDelta = diffDates(newProps.end, oldProps.end); + durationDelta = endDelta.subtract(startDelta); + } + else { + durationDelta = null; + } + + // gather all non-date-related properties + $.each(newProps, function(name, val) { + if (isMiscEventPropName(name)) { + if (val !== undefined) { + miscProps[name] = val; + } + } + }); + + // apply the operations to the event and all related events + undoFunc = mutateEvents( + clientEvents(event._id), // get events with this ID + clearEnd, + newProps.allDay, + startDelta, + durationDelta, + miscProps + ); + + return { + dateDelta: startDelta, + durationDelta: durationDelta, + undo: undoFunc + }; + } + + + // Modifies an array of events in the following ways (operations are in order): + // - clear the event's `end` + // - convert the event to allDay + // - add `dateDelta` to the start and end + // - add `durationDelta` to the event's duration + // - assign `miscProps` to the event + // + // Returns a function that can be called to undo all the operations. + // + // TODO: don't use so many closures. possible memory issues when lots of events with same ID. + // + function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) { + var isAmbigTimezone = t.getIsAmbigTimezone(); + var undoFunctions = []; + + // normalize zero-length deltas to be null + if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; } + if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; } + + $.each(events, function(i, event) { + var oldProps; + var newProps; + + // build an object holding all the old values, both date-related and misc. + // for the undo function. + oldProps = { + start: event.start.clone(), + end: event.end ? event.end.clone() : null, + allDay: event.allDay + }; + $.each(miscProps, function(name) { + oldProps[name] = event[name]; + }); + + // new date-related properties. work off the original date snapshot. + // ok to use references because they will be thrown away when backupEventDates is called. + newProps = { + start: event._start, + end: event._end, + allDay: allDay // normalize the dates in the same regard as the new properties + }; + normalizeEventRange(newProps); // massages start/end/allDay + + // strip or ensure the end date + if (clearEnd) { + newProps.end = null; + } + else if (durationDelta && !newProps.end) { // the duration translation requires an end date + newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); + } + + if (dateDelta) { + newProps.start.add(dateDelta); + if (newProps.end) { + newProps.end.add(dateDelta); + } + } + + if (durationDelta) { + newProps.end.add(durationDelta); // end already ensured above + } + + // if the dates have changed, and we know it is impossible to recompute the + // timezone offsets, strip the zone. + if ( + isAmbigTimezone && + !newProps.allDay && + (dateDelta || durationDelta) + ) { + newProps.start.stripZone(); + if (newProps.end) { + newProps.end.stripZone(); + } + } + + $.extend(event, miscProps, newProps); // copy over misc props, then date-related props + backupEventDates(event); // regenerate internal _start/_end/_allDay + + undoFunctions.push(function() { + $.extend(event, oldProps); + backupEventDates(event); // regenerate internal _start/_end/_allDay + }); + }); + + return function() { + for (var i = 0; i < undoFunctions.length; i++) { + undoFunctions[i](); + } + }; + } + + + /* Business Hours + -----------------------------------------------------------------------------------------*/ + + t.getBusinessHoursEvents = getBusinessHoursEvents; + + + // Returns an array of events as to when the business hours occur in the given view. + // Abuse of our event system :( + function getBusinessHoursEvents(wholeDay) { + var optionVal = options.businessHours; + var defaultVal = { + className: 'fc-nonbusiness', + start: '09:00', + end: '17:00', + dow: [ 1, 2, 3, 4, 5 ], // monday - friday + rendering: 'inverse-background' + }; + var view = t.getView(); + var eventInput; + + if (optionVal) { // `true` (which means "use the defaults") or an override object + eventInput = $.extend( + {}, // copy to a new object in either case + defaultVal, + typeof optionVal === 'object' ? optionVal : {} // override the defaults + ); + } + + if (eventInput) { + + // if a whole-day series is requested, clear the start/end times + if (wholeDay) { + eventInput.start = null; + eventInput.end = null; + } + + return expandEvent( + buildEventFromInput(eventInput), + view.start, + view.end + ); + } + + return []; + } + + + /* Overlapping / Constraining + -----------------------------------------------------------------------------------------*/ + + t.isEventRangeAllowed = isEventRangeAllowed; + t.isSelectionRangeAllowed = isSelectionRangeAllowed; + t.isExternalDropRangeAllowed = isExternalDropRangeAllowed; + + + function isEventRangeAllowed(range, event) { + var source = event.source || {}; + var constraint = firstDefined( + event.constraint, + source.constraint, + options.eventConstraint + ); + var overlap = firstDefined( + event.overlap, + source.overlap, + options.eventOverlap + ); + + range = ensureVisibleEventRange(range); // ensure a proper range with an end for isRangeAllowed + + return isRangeAllowed(range, constraint, overlap, event); + } + + + function isSelectionRangeAllowed(range) { + return isRangeAllowed(range, options.selectConstraint, options.selectOverlap); + } + + + // when `eventProps` is defined, consider this an event. + // `eventProps` can contain misc non-date-related info about the event. + function isExternalDropRangeAllowed(range, eventProps) { + var eventInput; + var event; + + // note: very similar logic is in View's reportExternalDrop + if (eventProps) { + eventInput = $.extend({}, eventProps, range); + event = expandEvent(buildEventFromInput(eventInput))[0]; + } + + if (event) { + return isEventRangeAllowed(range, event); + } + else { // treat it as a selection + + range = ensureVisibleEventRange(range); // ensure a proper range with an end for isSelectionRangeAllowed + + return isSelectionRangeAllowed(range); + } + } + + + // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist + // according to the constraint/overlap settings. + // `event` is not required if checking a selection. + function isRangeAllowed(range, constraint, overlap, event) { + var constraintEvents; + var anyContainment; + var peerEvents; + var i, peerEvent; + var peerOverlap; + + // normalize. fyi, we're normalizing in too many places :( + range = $.extend({}, range); // copy all properties in case there are misc non-date properties + range.start = range.start.clone().stripZone(); + range.end = range.end.clone().stripZone(); + + // the range must be fully contained by at least one of produced constraint events + if (constraint != null) { + + // not treated as an event! intermediate data structure + // TODO: use ranges in the future + constraintEvents = constraintToEvents(constraint); + + anyContainment = false; + for (i = 0; i < constraintEvents.length; i++) { + if (eventContainsRange(constraintEvents[i], range)) { + anyContainment = true; + break; + } + } + + if (!anyContainment) { + return false; + } + } + + peerEvents = t.getPeerEvents(event, range); + + for (i = 0; i < peerEvents.length; i++) { + peerEvent = peerEvents[i]; + + // there needs to be an actual intersection before disallowing anything + if (eventIntersectsRange(peerEvent, range)) { + + // evaluate overlap for the given range and short-circuit if necessary + if (overlap === false) { + return false; + } + // if the event's overlap is a test function, pass the peer event in question as the first param + else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { + return false; + } + + // if we are computing if the given range is allowable for an event, consider the other event's + // EventObject-specific or Source-specific `overlap` property + if (event) { + peerOverlap = firstDefined( + peerEvent.overlap, + (peerEvent.source || {}).overlap + // we already considered the global `eventOverlap` + ); + if (peerOverlap === false) { + return false; + } + // if the peer event's overlap is a test function, pass the subject event as the first param + if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { + return false; + } + } + } + } + + return true; + } + + + // Given an event input from the API, produces an array of event objects. Possible event inputs: + // 'businessHours' + // An event ID (number or string) + // An object with specific start/end dates or a recurring event (like what businessHours accepts) + function constraintToEvents(constraintInput) { + + if (constraintInput === 'businessHours') { + return getBusinessHoursEvents(); + } + + if (typeof constraintInput === 'object') { + return expandEvent(buildEventFromInput(constraintInput)); + } + + return clientEvents(constraintInput); // probably an ID + } + + + // Does the event's date range fully contain the given range? + // start/end already assumed to have stripped zones :( + function eventContainsRange(event, range) { + var eventStart = event.start.clone().stripZone(); + var eventEnd = t.getEventEnd(event).stripZone(); + + return range.start >= eventStart && range.end <= eventEnd; + } + + + // Does the event's date range intersect with the given range? + // start/end already assumed to have stripped zones :( + function eventIntersectsRange(event, range) { + var eventStart = event.start.clone().stripZone(); + var eventEnd = t.getEventEnd(event).stripZone(); + + return range.start < eventEnd && range.end > eventStart; + } + + + t.getEventCache = function() { + return cache; + }; + +} + + +// Returns a list of events that the given event should be compared against when being considered for a move to +// the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. +Calendar.prototype.getPeerEvents = function(event, range) { + var cache = this.getEventCache(); + var peerEvents = []; + var i, otherEvent; + + for (i = 0; i < cache.length; i++) { + otherEvent = cache[i]; + if ( + !event || + event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events + ) { + peerEvents.push(otherEvent); + } + } + + return peerEvents; +}; + + +// updates the "backup" properties, which are preserved in order to compute diffs later on. +function backupEventDates(event) { + event._allDay = event.allDay; + event._start = event.start.clone(); + event._end = event.end ? event.end.clone() : null; +} + +;; + +/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. +----------------------------------------------------------------------------------------------------------------------*/ +// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. +// It is responsible for managing width/height. + +var BasicView = fcViews.basic = View.extend({ + + dayGrid: null, // the main subcomponent that does most of the heavy lifting + + dayNumbersVisible: false, // display day numbers on each day cell? + weekNumbersVisible: false, // display week numbers along the side? + + weekNumberWidth: null, // width of all the week-number cells running down the side + + headRowEl: null, // the fake row element of the day-of-week header + + + initialize: function() { + this.dayGrid = new DayGrid(this); + this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's + }, + + + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method + + this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange + this.dayGrid.setRange(range); + }, + + + // Compute the value to feed into setRange. Overrides superclass. + computeRange: function(date) { + var range = View.prototype.computeRange.call(this, date); // get value from the super-method + + // year and month views should be aligned with weeks. this is already done for week + if (/year|month/.test(range.intervalUnit)) { + range.start.startOf('week'); + range.start = this.skipHiddenDays(range.start); + + // make end-of-week if not already + if (range.end.weekday()) { + range.end.add(1, 'week').startOf('week'); + range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards + } + } + + return range; + }, + + + // Renders the view into `this.el`, which should already be assigned + render: function() { + + this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible + this.weekNumbersVisible = this.opt('weekNumbers'); + this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; + + this.el.addClass('fc-basic-view').html(this.renderHtml()); + + this.headRowEl = this.el.find('thead .fc-row'); + + this.scrollerEl = this.el.find('.fc-day-grid-container'); + this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller + + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(this.hasRigidRows()); + }, + + + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill the dayGrid's rendering. + destroy: function() { + this.dayGrid.destroyDates(); + this.dayGrid.removeElement(); + }, + + + renderBusinessHours: function() { + this.dayGrid.renderBusinessHours(); + }, + + + // Builds the HTML skeleton for the view. + // The day-grid component will render inside of a container defined by this HTML. + renderHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + this.dayGrid.headHtml() + // render the day-of-week headers + '
' + + '
' + + '
' + + '
' + + '
'; + }, + + + // Generates the HTML that will go before the day-of week header cells. + // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. + headIntroHtml: function() { + if (this.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(this.opt('weekNumberTitle')) + + '' + + ''; + } + }, + + + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers. + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. + numberIntroHtml: function(row) { + if (this.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + this.dayGrid.getCell(row, 0).start.format('w') + + '' + + ''; + } + }, + + + // Generates the HTML that goes before the day bg cells for each day-row. + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. + dayIntroHtml: function() { + if (this.weekNumbersVisible) { + return ''; + } + }, + + + // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. + // Affects helper-skeleton and highlight-skeleton rows. + introHtml: function() { + if (this.weekNumbersVisible) { + return ''; + } + }, + + + // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + numberCellHtml: function(cell) { + var date = cell.start; + var classes; + + if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers + return ''; // will create an empty space above events :( + } + + classes = this.dayGrid.getDayClasses(date); + classes.unshift('fc-day-number'); + + return '' + + '' + + date.date() + + ''; + }, + + + // Generates an HTML attribute string for setting the width of the week number column, if it is known + weekNumberStyleAttr: function() { + if (this.weekNumberWidth !== null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }, + + + // Determines whether each row should have a constant height + hasRigidRows: function() { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + if (this.weekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + // Record the width for cells created later. + this.weekNumberWidth = matchCellWidths( + this.el.find('.fc-week-number') + ); + } + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit = this.opt('eventLimit'); + var scrollerHeight; + + // reset all heights to be natural + unsetScroller(this.scrollerEl); + uncompensateScroll(this.headRowEl); + + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.setGridHeight(scrollerHeight, isAuto); + + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + + if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? + + compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); + + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scrollerEl.height(scrollerHeight); + } + }, + + + // Sets the height of just the DayGrid component in this view + setGridHeight: function(height, isAuto) { + if (isAuto) { + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given events onto the view and populates the segments array + renderEvents: function(events) { + this.dayGrid.renderEvents(events); + + this.updateHeight(); // must compensate for events that overflow the row + }, + + + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.dayGrid.getEventSegs(); + }, + + + // Unrenders all event elements and clears internal segment data + destroyEvents: function() { + this.dayGrid.destroyEvents(); + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Dragging (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + return this.dayGrid.renderDrag(dropLocation, seg); + }, + + + destroyDrag: function() { + this.dayGrid.destroyDrag(); + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(range) { + this.dayGrid.renderSelection(range); + }, + + + // Unrenders a visual indications of a selection + destroySelection: function() { + this.dayGrid.destroySelection(); + } + +}); + +;; + +/* A month view with day cells running in rows (one-per-week) and columns +----------------------------------------------------------------------------------------------------------------------*/ + +var MonthView = fcViews.month = BasicView.extend({ + + // Produces information about what range to display + computeRange: function(date) { + var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method + var rowCnt; + + // ensure 6 weeks + if (this.isFixedWeeks()) { + rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays + range.end.add(6 - rowCnt, 'weeks'); + } + + return range; + }, + + + // Overrides the default BasicView behavior to have special multi-week auto-height logic + setGridHeight: function(height, isAuto) { + + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated + + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.rowCnt / 6; + } + + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + }, + + + isFixedWeeks: function() { + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated + if (weekMode) { + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed + } + + return this.opt('fixedWeekCount'); + } + +}); + +MonthView.duration = { months: 1 }; // important for prev/next + +MonthView.defaults = { + fixedWeekCount: true +}; +;; + +/* A week view with simple day cells running horizontally +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.basicWeek = { + type: 'basic', + duration: { weeks: 1 } +}; +;; + +/* A view with a single simple day cell +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.basicDay = { + type: 'basic', + duration: { days: 1 } +}; +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +var AGENDA_DEFAULTS = { + allDaySlot: true, + allDayText: 'all-day', + scrollTime: '06:00:00', + slotDuration: '00:30:00', + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true // a bad name. confused with overlap/constraint system +}; + +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; + +var AgendaView = fcViews.agenda = View.extend({ + + timeGrid: null, // the main time-grid subcomponent of this view + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null + + axisWidth: null, // the width of the time axis running down the side + + noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars + + // when the time-grid isn't tall enough to occupy the given height, we render an
underneath + bottomRuleEl: null, + bottomRuleHeight: null, + + + initialize: function() { + this.timeGrid = new TimeGrid(this); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view + + // the coordinate grid will be a combination of both subcomponents' grids + this.coordMap = new ComboCoordMap([ + this.dayGrid.coordMap, + this.timeGrid.coordMap + ]); + } + else { + this.coordMap = this.timeGrid.coordMap; + } + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method + + this.timeGrid.setRange(range); + if (this.dayGrid) { + this.dayGrid.setRange(range); + } + }, + + + // Renders the view into `this.el`, which has already been assigned + render: function() { + + this.el.addClass('fc-agenda-view').html(this.renderHtml()); + + // the element that wraps the time-grid that will probably scroll + this.scrollerEl = this.el.find('.fc-time-grid-container'); + this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this + + this.timeGrid.setElement(this.el.find('.fc-time-grid')); + this.timeGrid.renderDates(); + + // the
that sometimes displays under the time-grid + this.bottomRuleEl = $('
') + .appendTo(this.timeGrid.el); // inject it into the time-grid + + if (this.dayGrid) { + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(); + + // have the day-grid extend it's coordinate area over the
dividing the two grids + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); + } + + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller + }, + + + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill each grid's rendering. + destroy: function() { + this.timeGrid.destroyDates(); + this.timeGrid.removeElement(); + + if (this.dayGrid) { + this.dayGrid.destroyDates(); + this.dayGrid.removeElement(); + } + }, + + + renderBusinessHours: function() { + this.timeGrid.renderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.renderBusinessHours(); + } + }, + + + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + renderHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + this.timeGrid.headHtml() + // render the day-of-week headers + '
' + + (this.dayGrid ? + '
' + + '
' : + '' + ) + + '
' + + '
' + + '
' + + '
'; + }, + + + // Generates the HTML that will go before the day-of week header cells. + // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. + headIntroHtml: function() { + var date; + var weekText; + + if (this.opt('weekNumbers')) { + date = this.timeGrid.getCell(0).start; + weekText = date.format(this.opt('smallWeekFormat')); + + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(weekText) + + '' + + ''; + } + else { + return ''; + } + }, + + + // Generates the HTML that goes before the all-day cells. + // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. + dayIntroHtml: function() { + return '' + + '' + + '' + // needed for matchCellWidths + (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + + '' + + ''; + }, + + + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + slotBgIntroHtml: function() { + return ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. + introHtml: function() { + return ''; + }, + + + // Generates an HTML attribute string for setting the width of the axis, if it is known + axisStyleAttr: function() { + if (this.axisWidth !== null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + updateSize: function(isResize) { + this.timeGrid.updateSize(isResize); + + View.prototype.updateSize.call(this, isResize); // call the super-method + }, + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + // make all axis cells line up, and record the width so newly created axis cells will have it + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit; + var scrollerHeight; + + if (this.bottomRuleHeight === null) { + // calculate the height of the rule the very first time + this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); + } + this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary + + // reset all dimensions back to the original state + this.scrollerEl.css('overflow', ''); + unsetScroller(this.scrollerEl); + uncompensateScroll(this.noScrollRowEls); + + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + + eventLimit = this.opt('eventLimit'); + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } + + if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? + + scrollerHeight = this.computeScrollerHeight(totalHeight); + if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? + + // make the all-day and header rows lines up + compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); + + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate + // and reapply the desired height to the scroller. + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scrollerEl.height(scrollerHeight); + } + else { // no scrollbars + // still, force a height and display the bottom rule (marks the end of day) + this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case
goes outside + this.bottomRuleEl.show(); + } + } + }, + + + // Computes the initial pre-configured scroll state prior to allowing the user to change it + computeInitialScroll: function() { + var scrollTime = moment.duration(this.opt('scrollTime')); + var top = this.timeGrid.computeTimeTop(scrollTime); + + // zoom can give weird floating-point values. rather scroll a little bit further + top = Math.ceil(top); + + if (top) { + top++; // to overcome top border that slots beyond the first have. looks better + } + + return top; + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders events onto the view and populates the View's segment array + renderEvents: function(events) { + var dayEvents = []; + var timedEvents = []; + var daySegs = []; + var timedSegs; + var i; + + // separate the events into all-day and timed + for (i = 0; i < events.length; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } + else { + timedEvents.push(events[i]); + } + } + + // render the events in the subcomponents + timedSegs = this.timeGrid.renderEvents(timedEvents); + if (this.dayGrid) { + daySegs = this.dayGrid.renderEvents(dayEvents); + } + + // the all-day area is flexible and might have a lot of events, so shift the height + this.updateHeight(); + }, + + + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.timeGrid.getEventSegs().concat( + this.dayGrid ? this.dayGrid.getEventSegs() : [] + ); + }, + + + // Unrenders all event elements and clears internal segment data + destroyEvents: function() { + + // destroy the events in the subcomponents + this.timeGrid.destroyEvents(); + if (this.dayGrid) { + this.dayGrid.destroyEvents(); + } + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Dragging (for events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + if (dropLocation.start.hasTime()) { + return this.timeGrid.renderDrag(dropLocation, seg); + } + else if (this.dayGrid) { + return this.dayGrid.renderDrag(dropLocation, seg); + } + }, + + + destroyDrag: function() { + this.timeGrid.destroyDrag(); + if (this.dayGrid) { + this.dayGrid.destroyDrag(); + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(range) { + if (range.start.hasTime() || range.end.hasTime()) { + this.timeGrid.renderSelection(range); + } + else if (this.dayGrid) { + this.dayGrid.renderSelection(range); + } + }, + + + // Unrenders a visual indications of a selection + destroySelection: function() { + this.timeGrid.destroySelection(); + if (this.dayGrid) { + this.dayGrid.destroySelection(); + } + } + +}); + +AgendaView.defaults = AGENDA_DEFAULTS; + +;; + +/* A week view with an all-day cell area at the top, and a time grid below +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.agendaWeek = { + type: 'agenda', + duration: { weeks: 1 } +}; +;; + +/* A day view with an all-day cell area at the top, and a time grid below +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.agendaDay = { + type: 'agenda', + duration: { days: 1 } +}; +;; + +return fc; // export for Node/CommonJS +}); \ No newline at end of file diff --git a/RIGS/static/js/moment.min.js b/RIGS/static/js/moment.min.js new file mode 100755 index 00000000..024d488f --- /dev/null +++ b/RIGS/static/js/moment.min.js @@ -0,0 +1,7 @@ +//! moment.js +//! version : 2.9.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(a,b){return Bb.call(a,b)}function d(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function e(a){vb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function f(a,b){var c=!0;return o(function(){return c&&(e(a),c=!1),b.apply(this,arguments)},b)}function g(a,b){sc[a]||(e(b),sc[a]=!0)}function h(a,b){return function(c){return r(a.call(this,c),b)}}function i(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function j(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function k(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function l(){}function m(a,b){b!==!1&&H(a),p(this,a),this._d=new Date(+a._d),uc===!1&&(uc=!0,vb.updateOffset(this),uc=!1)}function n(a){var b=A(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=vb.localeData(),this._bubble()}function o(a,b){for(var d in b)c(b,d)&&(a[d]=b[d]);return c(b,"toString")&&(a.toString=b.toString),c(b,"valueOf")&&(a.valueOf=b.valueOf),a}function p(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Kb.length>0)for(c in Kb)d=Kb[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&C(a[d])!==C(b[d]))&&g++;return g+f}function z(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=lc[a]||mc[b]||b}return a}function A(a){var b,d,e={};for(d in a)c(a,d)&&(b=z(d),b&&(e[b]=a[d]));return e}function B(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}vb[b]=function(e,f){var g,h,i=vb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=vb().utc().set(d,a);return i.call(vb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function C(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function D(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function E(a,b,c){return jb(vb([a,11,31+b-c]),b,c).week}function F(a){return G(a)?366:365}function G(a){return a%4===0&&a%100!==0||a%400===0}function H(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[Db]<0||a._a[Db]>11?Db:a._a[Eb]<1||a._a[Eb]>D(a._a[Cb],a._a[Db])?Eb:a._a[Fb]<0||a._a[Fb]>24||24===a._a[Fb]&&(0!==a._a[Gb]||0!==a._a[Hb]||0!==a._a[Ib])?Fb:a._a[Gb]<0||a._a[Gb]>59?Gb:a._a[Hb]<0||a._a[Hb]>59?Hb:a._a[Ib]<0||a._a[Ib]>999?Ib:-1,a._pf._overflowDayOfYear&&(Cb>b||b>Eb)&&(b=Eb),a._pf.overflow=b)}function I(b){return null==b._isValid&&(b._isValid=!isNaN(b._d.getTime())&&b._pf.overflow<0&&!b._pf.empty&&!b._pf.invalidMonth&&!b._pf.nullInput&&!b._pf.invalidFormat&&!b._pf.userInvalidated,b._strict&&(b._isValid=b._isValid&&0===b._pf.charsLeftOver&&0===b._pf.unusedTokens.length&&b._pf.bigHour===a)),b._isValid}function J(a){return a?a.toLowerCase().replace("_","-"):a}function K(a){for(var b,c,d,e,f=0;f0;){if(d=L(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&y(e,c,!0)>=b-1)break;b--}f++}return null}function L(a){var b=null;if(!Jb[a]&&Lb)try{b=vb.locale(),require("./locale/"+a),vb.locale(b)}catch(c){}return Jb[a]}function M(a,b){var c,d;return b._isUTC?(c=b.clone(),d=(vb.isMoment(a)||x(a)?+a:+vb(a))-+c,c._d.setTime(+c._d+d),vb.updateOffset(c,!1),c):vb(a).local()}function N(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function O(a){var b,c,d=a.match(Pb);for(b=0,c=d.length;c>b;b++)d[b]=rc[d[b]]?rc[d[b]]:N(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function P(a,b){return a.isValid()?(b=Q(b,a.localeData()),nc[b]||(nc[b]=O(b)),nc[b](a)):a.localeData().invalidDate()}function Q(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Qb.lastIndex=0;d>=0&&Qb.test(a);)a=a.replace(Qb,c),Qb.lastIndex=0,d-=1;return a}function R(a,b){var c,d=b._strict;switch(a){case"Q":return _b;case"DDDD":return bc;case"YYYY":case"GGGG":case"gggg":return d?cc:Tb;case"Y":case"G":case"g":return ec;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?dc:Ub;case"S":if(d)return _b;case"SS":if(d)return ac;case"SSS":if(d)return bc;case"DDD":return Sb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Wb;case"a":case"A":return b._locale._meridiemParse;case"x":return Zb;case"X":return $b;case"Z":case"ZZ":return Xb;case"T":return Yb;case"SSSS":return Vb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?ac:Rb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Rb;case"Do":return d?b._locale._ordinalParse:b._locale._ordinalParseLenient;default:return c=new RegExp($(Z(a.replace("\\","")),"i"))}}function S(a){a=a||"";var b=a.match(Xb)||[],c=b[b.length-1]||[],d=(c+"").match(jc)||["-",0,0],e=+(60*d[1])+C(d[2]);return"+"===d[0]?e:-e}function T(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[Db]=3*(C(b)-1));break;case"M":case"MM":null!=b&&(e[Db]=C(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b,a,c._strict),null!=d?e[Db]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[Eb]=C(b));break;case"Do":null!=b&&(e[Eb]=C(parseInt(b.match(/\d{1,2}/)[0],10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=C(b));break;case"YY":e[Cb]=vb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[Cb]=C(b);break;case"a":case"A":c._meridiem=b;break;case"h":case"hh":c._pf.bigHour=!0;case"H":case"HH":e[Fb]=C(b);break;case"m":case"mm":e[Gb]=C(b);break;case"s":case"ss":e[Hb]=C(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Ib]=C(1e3*("0."+b));break;case"x":c._d=new Date(C(b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=S(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=C(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=vb.parseTwoDigitYear(b)}}function U(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[Cb],jb(vb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[Cb],jb(vb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=kb(d,e,f,h,g),a._a[Cb]=i.year,a._dayOfYear=i.dayOfYear}function V(a){var c,d,e,f,g=[];if(!a._d){for(e=X(a),a._w&&null==a._a[Eb]&&null==a._a[Db]&&U(a),a._dayOfYear&&(f=b(a._a[Cb],e[Cb]),a._dayOfYear>F(f)&&(a._pf._overflowDayOfYear=!0),d=fb(f,0,a._dayOfYear),a._a[Db]=d.getUTCMonth(),a._a[Eb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];24===a._a[Fb]&&0===a._a[Gb]&&0===a._a[Hb]&&0===a._a[Ib]&&(a._nextDay=!0,a._a[Fb]=0),a._d=(a._useUTC?fb:eb).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Fb]=24)}}function W(a){var b;a._d||(b=A(a._i),a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],V(a))}function X(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function Y(b){if(b._f===vb.ISO_8601)return void ab(b);b._a=[],b._pf.empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Q(b._f,b._locale).match(Pb)||[],c=0;c0&&b._pf.unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),rc[f]?(d?b._pf.empty=!1:b._pf.unusedTokens.push(f),T(f,d,b)):b._strict&&!d&&b._pf.unusedTokens.push(f);b._pf.charsLeftOver=i-j,h.length>0&&b._pf.unusedInput.push(h),b._pf.bigHour===!0&&b._a[Fb]<=12&&(b._pf.bigHour=a),b._a[Fb]=k(b._locale,b._a[Fb],b._meridiem),V(b),H(b)}function Z(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function $(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function _(a){var b,c,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,c=b));o(a,c||b)}function ab(a){var b,c,d=a._i,e=fc.exec(d);if(e){for(a._pf.iso=!0,b=0,c=hc.length;c>b;b++)if(hc[b][1].exec(d)){a._f=hc[b][0]+(e[6]||" ");break}for(b=0,c=ic.length;c>b;b++)if(ic[b][1].exec(d)){a._f+=ic[b][0];break}d.match(Xb)&&(a._f+="Z"),Y(a)}else a._isValid=!1}function bb(a){ab(a),a._isValid===!1&&(delete a._isValid,vb.createFromInputFallback(a))}function cb(a,b){var c,d=[];for(c=0;ca&&h.setFullYear(a),h}function fb(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function gb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function hb(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function ib(a,b,c){var d=vb.duration(a).abs(),e=Ab(d.as("s")),f=Ab(d.as("m")),g=Ab(d.as("h")),h=Ab(d.as("d")),i=Ab(d.as("M")),j=Ab(d.as("y")),k=e0,k[4]=c,hb.apply({},k)}function jb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=vb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function kb(a,b,c,d,e){var f,g,h=fb(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:F(a-1)+g}}function lb(b){var c,d=b._i,e=b._f;return b._locale=b._locale||vb.localeData(b._l),null===d||e===a&&""===d?vb.invalid({nullInput:!0}):("string"==typeof d&&(b._i=d=b._locale.preparse(d)),vb.isMoment(d)?new m(d,!0):(e?w(e)?_(b):Y(b):db(b),c=new m(b),c._nextDay&&(c.add(1,"d"),c._nextDay=a),c))}function mb(a,b){var c,d;if(1===b.length&&w(b[0])&&(b=b[0]),!b.length)return vb();for(c=b[0],d=1;d=0?"+":"-";return b+r(Math.abs(a),6)},gg:function(){return r(this.weekYear()%100,2)},gggg:function(){return r(this.weekYear(),4)},ggggg:function(){return r(this.weekYear(),5)},GG:function(){return r(this.isoWeekYear()%100,2)},GGGG:function(){return r(this.isoWeekYear(),4)},GGGGG:function(){return r(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return C(this.milliseconds()/100)},SS:function(){return r(C(this.milliseconds()/10),2)},SSS:function(){return r(this.milliseconds(),3)},SSSS:function(){return r(this.milliseconds(),3)},Z:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+":"+r(C(a)%60,2)},ZZ:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+r(C(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},x:function(){return this.valueOf()},X:function(){return this.unix()},Q:function(){return this.quarter()}},sc={},tc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"],uc=!1;pc.length;)xb=pc.pop(),rc[xb+"o"]=i(rc[xb],xb);for(;qc.length;)xb=qc.pop(),rc[xb+xb]=h(rc[xb],2);rc.DDDD=h(rc.DDD,3),o(l.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a,b,c){var d,e,f;for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=vb.utc([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=vb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.apply(b,[c]):d},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",_ordinalParse:/\d{1,2}/,preparse:function(a){return a},postformat:function(a){return a},week:function(a){return jb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},firstDayOfWeek:function(){return this._week.dow},firstDayOfYear:function(){return this._week.doy},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),vb=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=c,g._l=e,g._strict=f,g._isUTC=!1,g._pf=d(),lb(g)},vb.suppressDeprecationWarnings=!1,vb.createFromInputFallback=f("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),vb.min=function(){var a=[].slice.call(arguments,0);return mb("isBefore",a)},vb.max=function(){var a=[].slice.call(arguments,0);return mb("isAfter",a)},vb.utc=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=c,g._strict=f,g._pf=d(),lb(g).utc()},vb.unix=function(a){return vb(1e3*a)},vb.duration=function(a,b){var d,e,f,g,h=a,i=null;return vb.isDuration(a)?h={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(h={},b?h[b]=a:h.milliseconds=a):(i=Nb.exec(a))?(d="-"===i[1]?-1:1,h={y:0,d:C(i[Eb])*d,h:C(i[Fb])*d,m:C(i[Gb])*d,s:C(i[Hb])*d,ms:C(i[Ib])*d}):(i=Ob.exec(a))?(d="-"===i[1]?-1:1,f=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*d},h={y:f(i[2]),M:f(i[3]),d:f(i[4]),h:f(i[5]),m:f(i[6]),s:f(i[7]),w:f(i[8])}):null==h?h={}:"object"==typeof h&&("from"in h||"to"in h)&&(g=t(vb(h.from),vb(h.to)),h={},h.ms=g.milliseconds,h.M=g.months),e=new n(h),vb.isDuration(a)&&c(a,"_locale")&&(e._locale=a._locale),e},vb.version=yb,vb.defaultFormat=gc,vb.ISO_8601=function(){},vb.momentProperties=Kb,vb.updateOffset=function(){},vb.relativeTimeThreshold=function(b,c){return oc[b]===a?!1:c===a?oc[b]:(oc[b]=c,!0)},vb.lang=f("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return vb.locale(a,b)}),vb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?vb.defineLocale(a,b):vb.localeData(a),c&&(vb.duration._locale=vb._locale=c)),vb._locale._abbr},vb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Jb[a]||(Jb[a]=new l),Jb[a].set(b),vb.locale(a),Jb[a]):(delete Jb[a],null)},vb.langData=f("moment.langData is deprecated. Use moment.localeData instead.",function(a){return vb.localeData(a)}),vb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return vb._locale;if(!w(a)){if(b=L(a))return b;a=[a]}return K(a)},vb.isMoment=function(a){return a instanceof m||null!=a&&c(a,"_isAMomentObject")},vb.isDuration=function(a){return a instanceof n};for(xb=tc.length-1;xb>=0;--xb)B(tc[xb]);vb.normalizeUnits=function(a){return z(a)},vb.invalid=function(a){var b=vb.utc(0/0);return null!=a?o(b._pf,a):b._pf.userInvalidated=!0,b},vb.parseZone=function(){return vb.apply(null,arguments).parseZone()},vb.parseTwoDigitYear=function(a){return C(a)+(C(a)>68?1900:2e3)},vb.isDate=x,o(vb.fn=m.prototype,{clone:function(){return vb(this)},valueOf:function(){return+this._d-6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=vb(this).utc();return 00:!1},parsingFlags:function(){return o({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.utcOffset(0,a)},local:function(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(this._dateUtcOffset(),"m")),this},format:function(a){var b=P(this,a||vb.defaultFormat);return this.localeData().postformat(b)},add:u(1,"add"),subtract:u(-1,"subtract"),diff:function(a,b,c){var d,e,f=M(a,this),g=6e4*(f.utcOffset()-this.utcOffset());return b=z(b),"year"===b||"month"===b||"quarter"===b?(e=j(this,f),"quarter"===b?e/=3:"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:q(e)},from:function(a,b){return vb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(vb(),a)},calendar:function(a){var b=a||vb(),c=M(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this,vb(b)))},isLeapYear:function(){return G(this.year())},isDST:function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=gb(a,this.localeData()),this.add(a-b,"d")):b},month:qb("Month",!0),startOf:function(a){switch(a=z(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this},endOf:function(b){return b=z(b),b===a||"millisecond"===b?this:this.startOf(b).add(1,"isoWeek"===b?"week":b).subtract(1,"ms")},isAfter:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+this>+a):(c=vb.isMoment(a)?+a:+vb(a),c<+this.clone().startOf(b))},isBefore:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+a>+this):(c=vb.isMoment(a)?+a:+vb(a),+this.clone().endOf(b)a?this:a}),max:f("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=vb.apply(null,arguments),a>this?this:a}),zone:f("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",function(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}),utcOffset:function(a,b){var c,d=this._offset||0;return null!=a?("string"==typeof a&&(a=S(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._dateUtcOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.add(c,"m"),d!==a&&(!b||this._changeInProgress?v(this,vb.duration(a-d,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,vb.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?d:this._dateUtcOffset()},isLocal:function(){return!this._isUTC},isUtcOffset:function(){return this._isUTC},isUtc:function(){return this._isUTC&&0===this._offset},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(S(this._i)),this},hasAlignedHourOffset:function(a){return a=a?vb(a).utcOffset():0,(this.utcOffset()-a)%60===0},daysInMonth:function(){return D(this.year(),this.month())},dayOfYear:function(a){var b=Ab((vb(this).startOf("day")-vb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=jb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=jb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=jb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return E(this.year(),a.dow,a.doy)},get:function(a){return a=z(a),this[a]()},set:function(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else a=z(a),"function"==typeof this[a]&&this[a](b);return this},locale:function(b){var c;return b===a?this._locale._abbr:(c=vb.localeData(b),null!=c&&(this._locale=c),this)},lang:f("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(b){return b===a?this.localeData():this.locale(b)}),localeData:function(){return this._locale},_dateUtcOffset:function(){return 15*-Math.round(this._d.getTimezoneOffset()/15)}}),vb.fn.millisecond=vb.fn.milliseconds=qb("Milliseconds",!1),vb.fn.second=vb.fn.seconds=qb("Seconds",!1),vb.fn.minute=vb.fn.minutes=qb("Minutes",!1),vb.fn.hour=vb.fn.hours=qb("Hours",!0),vb.fn.date=qb("Date",!0),vb.fn.dates=f("dates accessor is deprecated. Use date instead.",qb("Date",!0)),vb.fn.year=qb("FullYear",!0),vb.fn.years=f("years accessor is deprecated. Use year instead.",qb("FullYear",!0)),vb.fn.days=vb.fn.day,vb.fn.months=vb.fn.month,vb.fn.weeks=vb.fn.week,vb.fn.isoWeeks=vb.fn.isoWeek,vb.fn.quarters=vb.fn.quarter,vb.fn.toJSON=vb.fn.toISOString,vb.fn.isUTC=vb.fn.isUtc,o(vb.duration.fn=n.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=q(d/1e3),g.seconds=a%60,b=q(a/60),g.minutes=b%60,c=q(b/60),g.hours=c%24,e+=q(c/24),h=q(rb(e)),e-=q(sb(h)),f+=q(e/30),e%=30,h+=q(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return q(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*C(this._months/12) +},humanize:function(a){var b=ib(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=vb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=vb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=z(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=z(a),"month"===a||"year"===a)return b=this._days+this._milliseconds/864e5,c=this._months+12*rb(b),"month"===a?c:c/12;switch(b=this._days+Math.round(sb(this._months/12)),a){case"week":return b/7+this._milliseconds/6048e5;case"day":return b+this._milliseconds/864e5;case"hour":return 24*b+this._milliseconds/36e5;case"minute":return 24*b*60+this._milliseconds/6e4;case"second":return 24*b*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(24*b*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+a)}},lang:vb.fn.lang,locale:vb.fn.locale,toIsoString:f("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale},toJSON:function(){return this.toISOString()}}),vb.duration.fn.toString=vb.duration.fn.toISOString;for(xb in kc)c(kc,xb)&&tb(xb.toLowerCase());vb.duration.fn.asMilliseconds=function(){return this.as("ms")},vb.duration.fn.asSeconds=function(){return this.as("s")},vb.duration.fn.asMinutes=function(){return this.as("m")},vb.duration.fn.asHours=function(){return this.as("h")},vb.duration.fn.asDays=function(){return this.as("d")},vb.duration.fn.asWeeks=function(){return this.as("weeks")},vb.duration.fn.asMonths=function(){return this.as("M")},vb.duration.fn.asYears=function(){return this.as("y")},vb.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===C(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Lb?module.exports=vb:"function"==typeof define&&define.amd?(define(function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(zb.moment=wb),vb}),ub(!0)):ub()}).call(this); \ No newline at end of file diff --git a/RIGS/templates/RIGS/calendar.html b/RIGS/templates/RIGS/calendar.html new file mode 100644 index 00000000..b28a0f67 --- /dev/null +++ b/RIGS/templates/RIGS/calendar.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} +{% load static %} + + +{% block title %}Calendar{% endblock %} + +{% block css %} + + +{% endblock %} + +{% block js %} + {# #} + + + +{% endblock %} + +{% block content %} +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/RIGS/templates/RIGS/event_detail.html b/RIGS/templates/RIGS/event_detail.html index a0b8003a..9bec1384 100644 --- a/RIGS/templates/RIGS/event_detail.html +++ b/RIGS/templates/RIGS/event_detail.html @@ -1,13 +1,22 @@ -{% extends 'base.html' %} +{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %} {% block title %}Event {% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }} {% endif %}{% endblock %} +{% block buttons %} + {% if not request.is_ajax %} + + + {% endif %} +{% endblock %} + {% block content %} +
+ {% if not request.is_ajax %}

Event {% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}

-
+
@@ -26,7 +35,7 @@
- + {% endif %} {% if object.is_rig %} {# only need contact details for a rig #}
@@ -154,6 +163,7 @@
+ {% if not request.is_ajax %} + {% if not request.is_ajax %}
- + {% if event.is_rig %} + + {% endif %} - {% if perms.RIGS.add_invoice %} - + {% if event.is_rig %} + {% if perms.RIGS.add_invoice %} + + {% endif %} {% endif %}
Last edited at {{ object.last_edited_at|date:"SHORT_DATETIME_FORMAT" }} by {{ object.last_edited_by.name }}.
+ {% endif %} {% endif %} +
{% endblock %} + +{% if request.is_ajax %} + {% block footer %} +
+
+ Lasted edited at {{ object.last_edited_at|date:"SHORT_DATE_FORMAT" }} by {{ object.last_edited_by.name }} +
+ +
+ {% endblock %} +{% endif %} diff --git a/RIGS/urls.py b/RIGS/urls.py index 27c932f9..87f0e467 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -57,6 +57,7 @@ urlpatterns = patterns('', # Rigboard url(r'^rigboard/$', rigboard.RigboardIndex.as_view(), name='rigboard'), + url(r'^rigboard/calendar/$', rigboard.WebCalendar.as_view(), name='web_calendar'), url(r'^rigboard/archive/$', RedirectView.as_view(pattern_name='event_archive')), url(r'^event/(?P\d+)/$', @@ -122,8 +123,5 @@ urlpatterns = patterns('', url(r'^rig/show/(?P\d+)/$', RedirectView.as_view(pattern_name='event_detail')), url(r'^bookings/$', RedirectView.as_view(pattern_name='rigboard')), url(r'^bookings/past/$', RedirectView.as_view(pattern_name='event_archive')), - # Calendar may have gone away, redirect to the archive for now - url(r'^rigboard/calendar/$', - RedirectView.as_view(pattern_name='event_archive', permanent=False)), ) diff --git a/RIGS/views.py b/RIGS/views.py index 57feffaa..50366f76 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404 from django.core import serializers import simplejson from django.contrib import messages +import datetime from RIGS import models, forms @@ -194,6 +195,7 @@ class SecureAPIRequest(generic.View): 'person': models.Person, 'organisation': models.Organisation, 'profile': models.Profile, + 'event': models.Event, } perms = { @@ -201,6 +203,7 @@ class SecureAPIRequest(generic.View): 'person': 'RIGS.view_person', 'organisation': 'RIGS.view_organisation', 'profile': None, + 'event': 'RIGS.view_event', } ''' @@ -261,6 +264,71 @@ class SecureAPIRequest(generic.View): json = simplejson.dumps(results[:20]) return HttpResponse(json, content_type="application/json") # Always json + start = request.GET.get('start', None) + end = request.GET.get('end', None) + + if model == "event" and start and end: + # Probably a calendar request + start_datetime = datetime.datetime.strptime( start, "%Y-%m-%dT%H:%M:%SZ" ) + end_datetime = datetime.datetime.strptime( end, "%Y-%m-%dT%H:%M:%SZ" ) + all_objects = self.models[model].objects + results = [] + filter = Q(start_date__lte=end_datetime) & Q(start_date__gte=start_datetime) + objects = all_objects.filter(filter).select_related('person', 'organisation', 'venue', 'mic').order_by('-start_date') + for item in objects: + data = { + 'pk': item.pk, + 'title': item.name + } + + data['is_rig'] = item.is_rig + data['status'] = str(item.get_status_display()) + + if item.start_date: + data['start_date'] = item.start_date.strftime('%Y-%m-%d') + + if item.start_time: + data['start_time'] = item.start_time.strftime('%H:%M:%SZ') + + if item.end_date: + data['end_date'] = item.end_date.strftime('%Y-%m-%d') + + if item.end_time: + data['end_time'] = item.end_time.strftime('%H:%M:%SZ') + + if item.meet_at: + data['meet_at'] = item.meet_at.strftime('%Y-%m-%dT%H:%M:%SZ') + + if item.access_at: + data['access_at'] = item.access_at.strftime('%Y-%m-%dT%H:%M:%SZ') + + if item.venue: + data['venue'] = item.venue.name + + if item.person: + data['person'] = item.person.name + + if item.organisation: + data['organisation'] = item.organisation.name + + if item.mic: + data['mic'] = { + 'name':item.mic.get_full_name(), + 'initials':item.mic.initials + } + + if item.description: + data['description'] = item.description + + if item.notes: + data['notes'] = item.notes + + data['url'] = str(reverse_lazy('event_detail',kwargs={'pk':item.pk})) + + results.append(data) + json = simplejson.dumps(results) + return HttpResponse(json, content_type="application/json") # Always json + return HttpResponse(model) class ProfileDetail(generic.DetailView): diff --git a/templates/base.html b/templates/base.html index 496d372b..153ca8e2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,6 +42,7 @@ {% if perms.RIGS.view_invoice %} From 30210fe85a6f3c28fa71f62de73763e8c0609f9f Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 23:32:05 +0100 Subject: [PATCH 013/217] Added templates for password_change_form and password_change_done. Added button to profile_detail. --- RIGS/templates/RIGS/profile_detail.html | 23 +++-- .../registration/password_change_done.html | 20 +++++ .../registration/password_change_form.html | 85 +++++++------------ 3 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 templates/registration/password_change_done.html diff --git a/RIGS/templates/RIGS/profile_detail.html b/RIGS/templates/RIGS/profile_detail.html index a9d9bbaf..174e3f3b 100644 --- a/RIGS/templates/RIGS/profile_detail.html +++ b/RIGS/templates/RIGS/profile_detail.html @@ -3,19 +3,28 @@ {% block title %}RIGS Profile {{object.pk}}{% endblock %} {% block content %} -
-
+
+ +
+ +

{{object.name}}

{% if object.pk == user.pk %} - +
First Name
diff --git a/templates/registration/password_change_done.html b/templates/registration/password_change_done.html new file mode 100644 index 00000000..de778b2e --- /dev/null +++ b/templates/registration/password_change_done.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load i18n %} +{% load url from future %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block title %}{% trans 'Password change successful' %}{% endblock %} + +{% block content %} +
+

{% trans 'Password change successful' %}

+ +

{% trans "Your password has been changed" %}

+
+{% endblock %} diff --git a/templates/registration/password_change_form.html b/templates/registration/password_change_form.html index ea8abb41..273193a0 100644 --- a/templates/registration/password_change_form.html +++ b/templates/registration/password_change_form.html @@ -1,56 +1,33 @@ -{% extends "admin/base_site.html" %} -{% load i18n static %} +{% extends "base.html" %} +{% load widget_tweaks %} {% load url from future %} -{% block extrastyle %}{{ block.super }}{% endblock %} -{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} -{% block breadcrumbs %} - -{% endblock %} - -{% block title %}{% trans 'Password change' %}{% endblock %} - -{% block content %}
- -
{% csrf_token %} -
-{% if form.errors %} -

- {% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} -

-{% endif %} - -

{% trans 'Password change' %}

- -

{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}

- -
- -
- {{ form.old_password.errors }} - {{ form.old_password }} -
- -
- {{ form.new_password1.errors }} - {{ form.new_password1 }} -
- -
-{{ form.new_password2.errors }} - {{ form.new_password2 }} -
- -
- -
- -
- - -
-
- + +{% block title %}Change Password{% endblock %} + +{% block content %} + +
+

Change Password

+ {% if form.errors or supplement_form.errors %} +
+ Please correct the error(s) below. + {{form.errors}} + {{supplement_form.errors}} +
+ {% endif %} +

Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly.

+ +
+
{% csrf_token %} + {% for field in form %} +
+ +
+ {% render_field field class+="form-control" placeholder=field.label %} +
+
+ {% endfor %} +

+
+
{% endblock %} From c1937eb2d7f380be64ccb98719cef374b370b428 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 19 Apr 2015 23:40:12 +0100 Subject: [PATCH 014/217] Fixed issue #61 - caused by typo in 0940d9f --- RIGS/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/forms.py b/RIGS/forms.py index ab85a460..640a2a15 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -58,7 +58,7 @@ class EventForm(forms.ModelForm): self.fields['end_date'].widget.format = '%Y-%m-%d' self.fields['access_at'].widget.format = '%Y-%m-%dT%H:%M:%S' - self.fields['access_at'].widget.format = '%Y-%m-%dT%H:%M:%S' + self.fields['meet_at'].widget.format = '%Y-%m-%dT%H:%M:%S' def init_items(self): self.items = self.process_items_json() From e3145ef98662ee291c5040127e49668cc2e573b0 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 20 Apr 2015 00:03:40 +0100 Subject: [PATCH 015/217] Rig numbers for all invoice lists now link to event_detail page - issue #65 --- RIGS/templates/RIGS/event_invoice.html | 2 +- RIGS/templates/RIGS/invoice_list.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index c3b42807..2a460f41 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -37,7 +37,7 @@ danger {% endif %} "> - N{{ object.pk|stringformat:"05d" }} + N{{ object.pk|stringformat:"05d" }} {{ object.end_date }} {{ object.name }} diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index 9db8feb7..2bb7d271 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -24,8 +24,8 @@ {% for object in object_list %} - {{ object.pk }} - {{ object.event }} + {{ object.pk }} + N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }} {{ object.invoice_date }} {{ object.balance|floatformat:2 }} From 3ce3a4ba67fffec17961d22ce4cc299590a718ee Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 20 Apr 2015 00:15:10 +0100 Subject: [PATCH 016/217] Fix for issue #57 --- RIGS/templates/RIGS/event_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/templates/RIGS/event_table.html b/RIGS/templates/RIGS/event_table.html index bddced3d..60aa7afd 100644 --- a/RIGS/templates/RIGS/event_table.html +++ b/RIGS/templates/RIGS/event_table.html @@ -69,7 +69,7 @@ {{ event.start_date|date:"(Y-m-d)" }}
{% endif %} - {% if event.end_time and event.start_time != event.end_time %} + {% if event.end_time and (event.start_time != event.end_time or event.start_date != event.end_date) %}
Event ends
{{ event.end_time|date:"H:i" }}
From c224866a50388e4101242637f7720988460d8b37 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 20 Apr 2015 00:59:00 +0100 Subject: [PATCH 017/217] Actual fix for #57 (Django templates don't like parenthesis in logic) --- RIGS/templates/RIGS/event_table.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/templates/RIGS/event_table.html b/RIGS/templates/RIGS/event_table.html index 60aa7afd..cf87f53d 100644 --- a/RIGS/templates/RIGS/event_table.html +++ b/RIGS/templates/RIGS/event_table.html @@ -69,13 +69,13 @@ {{ event.start_date|date:"(Y-m-d)" }}
{% endif %} - {% if event.end_time and (event.start_time != event.end_time or event.start_date != event.end_date) %} + {% if event.end_time%}{% if event.start_date != event.end_date or event.start_time != event.end_time %}
Event ends
{{ event.end_time|date:"H:i" }}
{{ event.end_date|date:"(Y-m-d)" }}
- {% endif %} + {% endif %}{% endif %}
{% endif %} From 2930595b03f5c479d104c950d69c327ff0e6823a Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 20 Apr 2015 12:57:42 +0100 Subject: [PATCH 018/217] Added api_key database migrations. Also includes other outstanding migrations: - Change Meta options on invoice - Add field api_key to profile - Alter field collector on event - Alter field initials on profile --- RIGS/migrations/0021_auto_20150420_1155.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 RIGS/migrations/0021_auto_20150420_1155.py diff --git a/RIGS/migrations/0021_auto_20150420_1155.py b/RIGS/migrations/0021_auto_20150420_1155.py new file mode 100644 index 00000000..269f9bc1 --- /dev/null +++ b/RIGS/migrations/0021_auto_20150420_1155.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0020_auto_20150303_0243'), + ] + + operations = [ + migrations.AlterModelOptions( + name='invoice', + options={'ordering': ['-invoice_date'], 'permissions': (('view_invoice', 'Can view Invoices'),)}, + ), + migrations.AddField( + model_name='profile', + name='api_key', + field=models.CharField(max_length=40, null=True, editable=False, blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='event', + name='collector', + field=models.CharField(max_length=255, null=True, verbose_name=b'Collected By', blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='profile', + name='initials', + field=models.CharField(max_length=5, unique=True, null=True), + preserve_default=True, + ), + ] From 7a24883ea7911f6d88c1a55e723352311bd5588b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 20 Apr 2015 13:48:18 +0100 Subject: [PATCH 019/217] Fixed permissions - now has same access requirements as rig archive (login_required) --- RIGS/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/urls.py b/RIGS/urls.py index 87f0e467..aa9023c3 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -57,7 +57,7 @@ urlpatterns = patterns('', # Rigboard url(r'^rigboard/$', rigboard.RigboardIndex.as_view(), name='rigboard'), - url(r'^rigboard/calendar/$', rigboard.WebCalendar.as_view(), name='web_calendar'), + url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'), url(r'^rigboard/archive/$', RedirectView.as_view(pattern_name='event_archive')), url(r'^event/(?P\d+)/$', From 1f1d5db72a68743356668f299d89fcc7333c81cb Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 20 Apr 2015 15:43:02 +0100 Subject: [PATCH 020/217] Add url to description --- RIGS/ical.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/RIGS/ical.py b/RIGS/ical.py index 6f0c72f7..55362d75 100644 --- a/RIGS/ical.py +++ b/RIGS/ical.py @@ -1,6 +1,7 @@ from RIGS import models, forms from django_ical.views import ICalFeed from django.db.models import Q +from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch import datetime @@ -102,14 +103,17 @@ class CalendarICS(ICalFeed): if item.description: desc += 'Event Description:\n'+item.description+'\n\n' if item.notes: - desc += 'Notes:\n'+item.notes + desc += 'Notes:\n'+item.notes+'\n\n' + base_url = "https://pyrigs.nottinghamtec.co.uk" + desc += 'URL = '+base_url+str(reverse_lazy('event_detail',kwargs={'pk':item.pk})) return desc def item_link(self, item): # Make a link to the event in the web interface - return '/event/'+str(item.pk)+'/' + base_url = "https://pyrigs.nottinghamtec.co.uk" + return base_url+str(reverse_lazy('event_detail',kwargs={'pk':item.pk})) # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though # return '' From 3884519bb14f65dccaa9b0226d6a0b377638f278 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 21 Apr 2015 11:03:58 +0100 Subject: [PATCH 021/217] Changed top to match headed paper --- RIGS/static/imgs/paperwork/corner.jpg | Bin 0 -> 49234 bytes RIGS/static/imgs/paperwork/tec-logo.jpg | Bin 0 -> 13526 bytes RIGS/templates/RIGS/event_print.xml | 29 +++++++++++++----------- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 RIGS/static/imgs/paperwork/corner.jpg create mode 100644 RIGS/static/imgs/paperwork/tec-logo.jpg diff --git a/RIGS/static/imgs/paperwork/corner.jpg b/RIGS/static/imgs/paperwork/corner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90aa1d5d997db42d01674eca7b3f8f53a35bc8aa GIT binary patch literal 49234 zcmeFa2UHZz5->W;E*Z&66jp*D5_ZX1a?VMDgk@QBUO=J*F%ncV2nY(If=HC0ARv-6 zh@fCX5fBg+P!Z+zF2;Mm@4MdrKkuA(&b!T-o}TKe>ZYkc*-I~~X3eoFn>1shR z5*Q^TB_jlFJ)>9E4E1n@AVWh)2!bFAh#bZU!9fWI{va4TL`p0}kTZh zr2zi3OvHpBG8p`uykx)X1QYtH6HNS9ok(EcdBEgJe(X`nXMRy*tfkdhQeOh8~lf`dVWASENGproP(L*w^K7(@d9UP%wZ5HL6if`pWu zjDi$SBLONI;D|kpXi`;E3=>a4tT-9-xxz{{UcQ}Xt)*Nnq|cDIqqF`{N2&unfF=(X+_6s(%Z*gtQObayZ>^{$~EM8#^swG zwNI)0|P`t0;EL{Wg;gdl_H9;2hE585(Mxtk&4HjVGJci zBy%D!2eYm(K;-utna##;sqs^ec?qM;%vi85%nn;TfA&;v3zGbNsMx{xmM@3Si!-0! zA4xSO%|P94Kf}C?lSvBZy@TMpY~&MC#R;ES?liGFqHR!Bz2Gc6U*3I!?ql5+^kO*A z@?(2*bwl13^j>-kdXjU-$XsOTv*WXwFBWNI_ZD(C!W>3u!q2gPw0+WNC4mZf@3o^Q z;>1wp9o;+k18qX3Ty{KB8@}85BKnPx%ko4KhGuP=|Cj;slMh(4Q=BXME4DmuJw zx7^N&1M}ereYPMwFT07b8du8&v(Vu_dAX7g4$Qezk$q9+BoDp0Uw=6$h5sZw8O6(A zXZGA{FB|6RnN^wCEyyC}e3WsKPi~^teDA#97kl&;WV!|UTnx&;zOdBU6?A(GlJa}A z1@S*wx$x<&eam2p^aa!W2X*@U6>ET+(E+a&N>}@DRlV)Fpf^j$73CT|H~2Y#|G?JJ zcE|9g=<4b6rI@AY@}(7lo)T8!p)I5wYn`?^`ZO^`Pwml31@EDcTTuA1k4e#ucf1;s z@9kdn>bS!bPd$$c_SDm~GS7-qc%gOv4DZI1Pm&RHr94d;l;l?Rfpl*qi*c5ZyWdRS zJzIJZmvv)xk#S)7(Ir>Xt@`Qag(Kpr(-H&-vv+Dg(d||qK35Ort47R&-KclE* zYk18fQ)#WV+->#qhMU2BM^4p7)4=81Wmi`{X?$Nr*xD?(JZh+YZvJ)Gm@Pj1iB8Lc zoTK#)o6wKKGUn$_w)Ki!zaE@PjTVk5z1A}`xzX9guW(Ax+{^d!tsDxUBs$nbdQO>$ zv=Oru>pbH!SLZLTb9v%<7jO%Py>g^2nMdMo+8r$Va@Vin>lWnsFn@Z)Y@YAxfon5$ zWe10Qh2?h5+!0iS%^ycE7Wd;n_^!!39PHCI9LsPU7rmZ3B5#>Jk(Io_QhC2JD>1+; z-#bF+xDRe%YWnjQbl!4#;Zx$qv!vli)r*-eoMH6Aq~`MtHlGCcFV(wI4|hdb zzsGv7UG)yPIWjogIqB(D8(Yw>{HF!jhYJqw)v74P zQ(I889KRI$Mm2oT*pp(H&u1`-wohItyuZJ=DfB*|-)Q*mwA~gIJbmV?p>znc>zdu> zYbngzx0tV0uFTDYugWE^J)H^Xwvnd}yuL8adl^HsiT61ZcDHxO&Bm4E>YPg9 zU&Kbm6QVU824}zc+4d^r-#M&t^Zd%jO~Jm)Ij@`68Y|vzXy4gYa`5(N-|0KkpH64& zJ$#?Wz;(6EKm;>6jvF&H(Q9rLVIOd$0M5ng?>cHg_wrpWoL%&l+kk@Y+$c zz0YeILG?w)Vrp%yafewF6NZ5l=z}r>rk{cGR(m<|d{d)Le+R4cyn?h3&P& ziC(>dMMsH=R9canLIo>y>92#8Id<{movK& zvh+1#aV^?Ug1W@!p!)n1YxL6mME3fmVbW_Ew-=4C#f9<8!%3MY9eD4xkmlT6NAuCb z{MrliW%lLJ#*L#|JHBvS+PFF4TMer*x2_+TzMq~h z)#?Rvk)DlKZ9_~Q*F)RQoO!P$v8MY=&A0V>-(9N?(Vjm%W1g`EjYKP^cYj)#GV#+zzCb9|-qySN3dq?czm9W1EL4w`v--oHFzapAC~ z<&pS9(znF+KYmhsFw^?+XZIWX*P=&p8hX@K z%6HBji||-rS-b3#>sZE`O4+&A|;j zrPJ1O+!h^dS9bpQ2{Qxl0_Jg0I*znDF#wws?E ze0|4qY+Cgu%O>vY4g28}_c^_|N5UOMEEM=BC7+m-?bw1`@a|o><>p!BTz)@9qaC3CQ#pn9{udZWhpQ3zyK7CkyT!??W zvFq@-L(#w`F9%Or{QlyP)(0I%q(?K|u5>>5^!Y&R#*vFn;}sj)8a)e590dn_rM_@T ze=Ui5#JMqFndmq1?!=YXN{R^|HpEJ!-z#U%0e@@%C6A+M^31se*PXPTy%p1`rq^$I z*S5<&c-Y_kxelGYX8%UDZKGkxA1!kCZp+28$k;lbiWbeI(@S?kUfHic*?g`Sma7~) zy5VTPapS^iVYbPLh5qi~Gx!^{cW>=^;<3j0#(Z_Psw4W+*Lz-dNCt8flVq+vl1zp! z&tE5ooEiRX=vcS~X>SOG9~tb^($#@UHq#C7a~TqGojnyg(|2TH{{+{>=QBCG#gH4m zF9N=F?@Q+YdMQNq^P{N`R2R%TrL3nKYsP$&2R1?Hc+JUKcVn;BI6C5QbZ$WsRtL1t z888S}sB$x>Z9!Y3tBH@3M)6BN4M7{pUWRT{>$Uyzi_MKwJ@1rczNVb9zZsSPWYl#2 zVWFGK-1ubFO=t9AZ^_~(#Nhn?HsrUU0C&EW+z&yb8LxNTC~E4s z8*$hMdsH~!yl^MBY%*!#bCYBBhQp1kepfe_PE{Vcv6`!YK1%X}{f%84<2JYqxo4Rs zk3_y+rd0X4k z@G$PR$d@-yx*DE5Iyjw4!1qRJ{-5bsistjER0z40eoTf4yV; z!TS3alBcPWJX!59ELekkfIGn4pBBM2XTelT$NNQ3FM8|#A9C4;_$(;F*hW zNWdCC{-=br&!jGhm@+$-?q1oqZ|lLIlA#&*$m{O=yM6J$X-NPWZ%uA3k&*m-iI)&X zL9%MPh6sM*TO&O&|0N^T!I!q{C>hQi0t4<^=A> zL>PG9fe8-s)5iJY0x>~27XW}P!u)aHkg2wj2@pb!u8$iM-2UIzA08a&ZJ}vx0h;<| zT!Qg`hZy4BaKAywFy28H7`NYGv{)RV!-WRv;&m(x4V?Y_ya@~`zJq^5rgZlU3|IB` zaQkj4x^3lkzJmcJxeLw}6YL!Xij*O^z@WcCxB3qL9i7_QP2JDiFYp)RGHml#)BaHb zNFXCWUxKYEgZ%uBgM;w6Uk47AHyB9&2BmTK3jzb`-{91si{1YfojCIZ2@OFs;37G& zLM(63GD0YIdk9k!@%9j(!$1xr*o_rZgYEWWpb5P0=8(Cn( zy|8jFJO`AKilOqMKK?$qAPh3p$J-Y#AF9MdWG)ZN1h5znl86%IrNl!Fup+GuO_1t- zfjFd;C`ts2l13q=(V}PxNf{X#VI&A8qr_0+V&ak_Xc>7)33(}5l{eGS z`qmbBQsVih)ZxR2MGs4e`USd)q2=V{h&;qa0EP%Y%r^)VD&mXh{mwxHhsOqb_y>9T z`63A%F~BngDe(YFw^i`*|H1YzZS@_|1^WZfA51=C<1Sb++_xw>uq&FNK9Q;a_aOQY zlHXYWs3vyCcOqa~L&Lu&_}*_mK0lhk2Wf@?HU6oW@q~#aW`@K21qWhrnjs*V&-=Zx zAP?7{=KM`KLd|b-;lTHZzix<7^8?jiUOfxRCGN|8pbq*IyF|;(Q~-X#nS{ z#6$SJAcB$<5l35~#N{Q#|r5M0UZ- zyZQzCV1PM2d@yb}F;9P-8xQhFFUb>~4`>;PC-Ey>S6$sC(9hMw8&nXN2M;XLOj`~4 z4M>#O&`@627axT2#o}}|lz4!;q8=VD@+ehV6dElfiINdVNz0JCwD+H(E;fBK#piUkUqb?z(rl|(HoG{SkO*}$z z-qso(pgZtHd&r?Bf333l%PQj7F$c~Tj1g%mDG8}>B)=;vAuTH{DJ+SSlSUD9pn?oq zd^;B>Lbh`W0GC2<<46hOCD6*r5^&^X2{>|cL>xIZu}t75N8~0aM`%%w$Wx9W z32+qB!k|lq!7vb(7M2l~6_yhQra_~H(c;2r31PIPFz8}{gq9Ho<3SwYf>8nn2^c?8 zU?_o+00s>h3ktw0p#LSnNK-{gYH3NSp;YCh#HBSPWu!EurBt;DO9|0C%jv=k52LLEb=ez|Nq7!-+Bz|j`4NFfen!u4?!U@!q#ECzs3G_C=ly@*=Br4 z2YVMn@yDh~`TOSPfA{}c;C~kQp9TJBf&W?He-`-vkp+H+U2wi&YjGF^Rkog!*Ms*+ zM-ww+EnNdmVibw|3dY~xn*<3#KE6SLgf~iS8(Sp#B#5lCLDUdCM2*4X{Z&m&3_t`3 zBH$A4zo+%_?ZDuu$Y0n0j{PrB)Gi=s0)k6~@Ux!_9t(n^uuuph^~U-Kg1{cx3jmir z9OMtm%p5D0{Yx(Di*eJdAWF`DL} z%m?6I{y}O4_+C(64EC~A1!Zt*lZ4SVP|I=~pF}a--Qpl9?*ecak04!(Z}4zmXCq@! z2DBs>z5MhEdLR)}8yu&}pq=N!2zR@CVT|KmPzu`yvdlSbZkPT537;Hw64e&<{VFER^ zK^gEtyu1yRv2AWCWmX+vt@?higz_(86a z2Z)+#6Jw@?&leuB9gr7*{WYEmzzh6F^Ap33NIgfO2A@Ly8fUwO`ENLW2o?kiAptH0 zgaJYcA&ZcMkca~a6@)TE13={v3O^(;0W{y`L_%)RJkVpCPXH7QS|-S01*x574;K7E zgNzGw^uP|DSpm}gr1tw+5BLzG#6*pWpKpG#0EirW{lFCY)lQv}w^@JGAnzmU4~^1v zMncBKnDjr`m-Gec9O-?KPm)gkq$hnuItBhFL5$iP{1GG~#I8Mva^ZjAiTtKRE5Hnb zFd%;h@CecGpZ)MlkGlGvW&c@2umtXe(Yg^7C;StNdVpF&-A5IoicqhAYvU)!->d{r zh1bHH;0^Fbcprp>x4>KBciZb@9q6+YaHZFt47N~ z%S)?4%T3EqYw!aLNy|curq!Vp05u#xMy~hI`rmZ%fNX%RzFYOL+ak;W56BWo;sG5d zjtM-`!oE-#;QEVIzqf(lLZA`4|1xT231j&?2AL+A2AL{^BoiW&BSVwv5z60Z9+?2B zlLNWt59zmO&JT>=G{F6W1+4Oej|g$p5ymQkIt0k=4Mqb9BZPvr1A^53{KJT!V8FKk zlnhc8EM+*Pt}j+p7>V)rMiQ&=$Uqz(7YJepq7Y#nB+x<7do$wt2HQ3G6Bc9$K}yTO zXAS*?*#<*U8TiV=BKZ?02z=ts3lLNyNRCND`2g}>Q!E$&C z_=ACy*xLuo+o4{9{m5Y;`;oq#87Bef%zwbX73jgL56*DI$w*1T*>X_5eZU;-5a^Lk zGL$F=G$WI=3G+@VS(QDKyI6*JDtIMWL2KaHHwhV>nu3g!21W{wtiu@)&>luoG#Nco z)s%+`6Cmys3l5)G@m4bjk)vAG%mO9w!6w7c_-b~ltT@L>zD>ETD10k}N#%zAnbT6a9Q0d_&uPA$c(B>n?0!N{af z6ts4N3$qz){K^s{Z=)2}Pib#hf#`}_WsMnHAV@_}vso{OGHyl`J^==oF!p(`VvJQ{ zzGVwUqS8v50i*9bZmJvuDY6E2Lfi&LxTABwv4mhR()o4O4t<0Wt%}CyRDf!lA|v_G zUh1?P`7uCiSYnR}!kmYtautF-y?c4TGUW1JlB)&crcuKpVQGC;zq11?xVS+oKOF?0 zm$aZ99fBY;efP^i<+=UZ;DJ6d+=l!tb#8Y%kQSRtNe_u#Qb)K>{R)v!YCkvbhRL0$cq`3!Z6l`-Wbqp+|;N}1d_VGyT}A0 zm3{F10d$>%)=CAKO#@30tNjaBP$b`J4@OKXj4y@HK^qo&R~I1){wpRiJS;YjV8pt! zb{(g4)fH2L$d6KFO^}p(+W?hxv$}ebf6a%|`K(@yhR{6~2&OHEt6@kl6bMQU-os|BJU6B4PQZtpHBfcSSD7Kx!La4q-;9%_O`1&B}9N-=cD zYV62cj940^A?Ubs$|Q6f^X50SBtV{3ihj9xVmA zZq?laTd6+Hs+^f^3qdxsC}om2;wK?E3#;{`f`6okOncmw)7yfeFyZGX>2M2{F%auw zMT@v zsoWLsfSd6xL`F1xUfInW{@JZ0Gv46J((cZK3IPN)SkH{v^Q5)`@N zC3n`K+~e8lE-8Qwnv>=oA@Kbe zC3*W*04#hrk6bIErCor{sDmC6>0+#YD2s8*WzX!(4TD&h&_flhYG?zShz=pA-BX>!G4~Oy??pLo65a@4~ z=ZTbk=QH1+0#Q20gej-LbBE0Jncq?OU5|mOqTa4BX@!u-4T`}hq*!Mur;h5<>mH`_ZyRx%hG=ZOn0f}0O`?XHkLWREl8(lxInXQz$fD5o`N(l#IP&PrTlfQBw< zQ(u`6t7u`Zc|JUD?h+$1b87Eb)`?baSJms$Ep?LIq5kc$xNGswbB`g%xLa2&uVol$0|WuSz5q8VHY2)0;;AJ&qh5?xi2^6O*!`d0vWaZrs)9k{4PJ;lG*cl zSA}QJy3U<%#Ncx@BpTeL`n}~tT-5VjSrz01kaM~XEiciM!HTopCkI;D+dN{2FR6k4 zJ-G@ioQ*mE(5GR`uC*%{_7v}HXz1T5z zH0Aiv`842}2os}prwUBi8bNP=uLUB#;#}Yjk&(+aJC783w+^Sgl8n6DX`*_qQ$9hZ zKIx;30lTNWsRx_v#8JfVI6*g=9bWv26+!X+ot*toq-!{*_ZlVU&|`V899NbL!n7^Q zdwQ!t8IlI0;-f;d9eHeGaJ590U=vovD~1P0RZf!Y_A%pQ?!^yRoX|P_wp%^i@I1Lh z|8eOnzWnUEy$HZ3PzNZg@FG@CC61o2awTo5%M0xv7nLD$ot!gNGNUJJyK zr`+f7tw{!S5Tep!=kp2>VUC*?!u1g#P=<(_Q{!L(u?(FRj7dDCT>x`Q^S7d&fT&DQ z8q99j{9{gXQir6-3UEKurP8F=1u?dF3Aeo#QA=}i&aflp>(b<_p$@&H&)IieQ0PUg z7vr%Pj+zw?#q}88{X6kH*+O$goIePwy+ai;_h|7d=To%?)9Y_m_Lkjs>LW#7&v@n_ zSUhgSzij<-G>d=~4aTi7;1bM{Px|Xo|Ps|A9)EH#TR#Aucx5HUj zN>8$n@)01g6)`oO{;)vvTdGuRDqME!!Zqyg(RQ;^_WiGB-Eje@7z8l9MXF1)6^Ke*)NiJT>cpH#M zqL{;Me|d)8)?2siCFibZwlXiq0yAt#H?&dNNZ)gpkL#cthDm6^l=SU#wu0|! z6YrrSZEUOcC!dlG2|{I=S3nTJc<&%z>x-1VCpFZ&p)94dU3}*A%pWz746QWh_71vT z8eWOl;ycx*!_>}*Mx0bb1e#>=!{pVKQrcS=Q}%Wtu-(kRwLnR7lCteK#NQXLuADhZ zcu)R=L>-w<8k7g^8ulV7a*X){07^hAW- zLjT*2=RXo2w{uI`6OiAcnDsI3&szc;NGqWjk{Da)O6DZk`Ts$JJ-s>p8pP5mUz`k|5n3RguWT;R`WpyIT%mOWWeZG!us zHbWlwQ1P7p*$tQ|wz?ageK!{UIuh^PfrvG4trK zFrZ*7b-nR_)(FIiL+oU%`a@)}k)J|>xX0=r<1<&KP)#cTF*<#M_eaC@-(AZ;5i^sR zY#b_5`6ra$>Ppf~tf+u%{KFI~lKM>Y&>tcLG(o=X)droIKP4b+i>~kdQ*019u+we} zE&p@;x>ML;`~{UiB_O=u`7`}7I!xHE<=RfCKjjX6v?EV(um59o$V69S-aqw^@nQP= zkXfS0KgLf3!61`A#tsuX`c$;wkI{EM`)`<2?Lkr#c2a+qNKr*33x9?Run60N;6KKO z;_Xx#WBwSO`p`Rjl|RNNtylRkOh^*1`(N}AOnYDP*dI#*nN$5YMqwnS|1XR}sTT8J z7zNSboB8L%5JpqTLh1K>j`1&jpzSQxYgb&rAHO&~mj9}@fR zneby`(AOAD%iWB)unpBiUpSj_7qZ>B#fPHB$4fbT!^`}2Ey%NlG3!nyp|a$fd!G3| z9i%>#HGq{JP&%E_E>uyzKk=El=UCWc2R`vqgu@xZqLX9(ycBmA^JaYfpvaT;)$3K= zQ`yJ6`TWAgZacwZv+rLWKc;6%G5DG_qfyJU1LudIB2#j+f7lkFpG?Rua1)OwRy1*(_yIMG^j zR&+-}pMSfM{?j<*t2(v;akH2F{w2xGm2!{osqtNls}h0*n1|OmFX}azq4%Wia@CW) z)VgR@pz~CHm11>@mCU-aN;j9y0Cx@QUEw83EtfbWyDWKe zP!#Ww^H{WyK2A_7;j9~5N5Ds&1ZV~8X}Z!83!2xP4ag1Ilr_7DSjmj^FXqcjej5XC zn;235Sexg`HCAeIiJekz?@Be~9+j|a3LkP{;j??SjMi<5UkJmy^dr-I6 zv2wIJ0Vc+jLC8po7RAGEtOG149-Di8`ljz?KZNiGTE!c_lk5p)Kv=I+G0|Ts`N&q} z@5HA5dUw`-52lC|8F7VIQdNij%e0C~@+-VZErQ1Or!?>{51@06UvD1G?g+@w8VJZD zjbR`sDDEEvTN!Vk^&h{~+L|H$e!`|BJ!SW?WX7V@x6`>;*+cE8Tv^c9)sx%l^oCwG z&GUd?XW4?7G~SV0;rd>!d4(Ptia-=HO(@KoxG)C2q3N9%4?bb|;NDoxtv%t|Q^xT1i22Y z;M4jn_a30SIEn-#suSIqz)w%{<7K<>Lp)vhukVqYCSygrk3J0b#On?^o}h3j$QKCY zTgJ)``nSMIo^cplIz4n%L;31`t~8%2wSlJ^3@2`@QrzW>r>~}B%1wKmHF)b>Lr%uq z5;MK3hQ%boCzXZt(5Rz}0Zh-tXuPu1f}K)Cuu@m2nHrD0 zB3ms#wRdG_z@?4O7orUJhQrh-C87z9w8ZVGdD79Ws6>LVFRXbNmg~m0kQeX*ClM>~ z;ku7by{LhZ$vU$~d{Vi7;tKE>kDHn}&N0fKzU;l`9b!8Yc|_~nYVu1DCQ)O>8-r)g z-;Yz7;k0K-EljQZz;5c6sT`lxe(CMq;j@=cbI|V3rd7|snrp;+>VC?l-3=WQor3Rz z4_Tbj+q32qYW5_J*5eTDF~&)f(Oru;Z7*uNT~tPk?3vo#$!Hr_a{pP`(brsOMU!Pa z_~!V!6w>+kc!u4U{rGUO(^yL_QP4*=A4~$1ld3&#rsT(RY+9Gh&K|=xY8x5$&B!k3 z&X#bvBvyX{^Y%@uIz>S13(l;^$JjA@HC^}EKMvp*6l%Re;~`NNqSh$;jN@EEO5F#! zo4g^~TM&=L+cgSZ(_?~0#uKI;jC|!zGFbx>$q%%cU;AHYkJnoBh?e4Jy27s;^-i$u zU|TLny7ay|1`3i>eGQ_&%6=_OHNx-LG$uVi2f?ei9V%#}4ldSCp# z`CIvmY2>wLYP)vLwS^0>sXQ;d-SNO>8Tlr{S!+G-SvpoIzf$gG>I*o<-G>+_*;`Z) ze&4jScWay<($Bz=EI-nxdcZl?Hluy24irETA#dm-1}kV!Es3M#IBU3Ul6Fko56x5LOyBY{rK%dS$C|HD25 zwJIDMYm@w%E+a8^Ms}A1&NSZ4^}+FDi3yGLVcp3$F5`6RLq+s>(w?cAn~S=axu3p~P^5F*3tx2c2{L zOQC&}x#REU5jM0o+^Evsz31qk=BTw4CONA0v8kWZ)ekj(1YCA?!K9v>W3B_OReUY0 zWMPW(Da))Z4KCZ55HPo4FVD8~%rUsgVIx~VF5`U+7S6rQ(-$8&wUg_VM!@EY_Pi3p ztQ{J~>TmKCqCKHv^r;g=$H)ZOEj6!_I{{C*+QAn#AL}CL9I@FMF~j3lIM31qHYn{V*s!t%}G2vKt(^ZOi7+UF> zPDIh$kqeCZs#lZd*`wvoc?Kh{3EcE!;xU;3woMDwaH5Po6r>hq59C}u-rfEvKW{_L3$v|yW_+h?gK=Wn+cCwPkdM9$xEUVb1p^kkj zTC0wUBTc}>HszK}qNY-z;!*S6BfEK+^eRsB(uFOU7mH3$hX(O^eNhuV(^tGkaVr{a zu)ON(-oQ9kPPqjYOhnXdcwAI!UKU-xGPv1uV8MIxWWhO!Ce>+b>9h;y-NvU63T|Fh zpbO*NGpgj=WbrsLdQ2AiAy~LIP4aYHQS-eyJ%>DJbyM!)a}zqW6cxzz0p2VtV{6F- zs#9;Kz9d`NUSz6Q6poocc0op8V%SccFKPbNtoTe3OK#He5s5dQb!)KCw&Wiq4lErQ znPj@T@lO0hQ=RF+!@47nk);Q}TIFm(yXBjY3?2z#R zy{}8N4N7n*_u$QsZ(HfQ$5M~0j9wv$2~m*V^<4EHt7;zaARAc(K9QP9cG+%ckWs-{ ziT2~u;i?ZeYkc~5y_?Auo@Tw2y_b_h4SKgaX1&yaEDc`2Q%cdHG%bW5>YGGA!|hoKwi=qeiFxSAATACTHq{ z?ta?&V3tl9KS=}j{B@zV{fI?waPV1H8x|trT)s)+3e0ScK@Wi_Dc^G!gu#Nzd141KRW&Pg5ovpnwx6; zwXR=oV_X;XC->G)5e;B<-QaP?{Nuf$o}Z+j+l;7IE+u8O`{>`%R+Q~55(x}x){|NA z?Bpx2gTeEnFmH0F->Mm2u41@;rEYq{mu7kH0>@2tw<6|8_h!OwPwQXvq71fCNc8I) zUYa|G=?!V~y-I(W-0()#^r35p%jcxa&L~$KOzP-m3(9-nNcY@=dPW^*N)8TAz8REh zl4|aryngiXZCR?xC{FX5oz}hH zFt|6hYn7p@e~sjjSvKvYO`sGyYP9Bgr$xkgtAN=l&x43JLEWer&C86@0pK5z9 zRmwC6eRbY(xr*byImgqv%Hh0e>eqe*r?g*>uOrCUF6G|aMOG3gNA~!b<)q}jbytv2 z1^3?!-c1#-$~VJwo+Dgj{#J!5#an+*1Y^tI{;|)Zx7j;BNT*&Ci93H^RI+xGz0O*C z{8*-K>2-NCQ?pr0^567J)(z-f{{VhPIY_^!)bmJ6eY|ak z`gFR`<=`G4|NXwJTM$lU=Z#CF?s5Fafn@FU2O4q?RBOHQX3U7JsJN}a`}J;0+e*$} z74|_@=W#R7?$^3EQfCHZ=`|BL-h8~vyV-DvMI~(q|CxdTO4FtqkuwT&mS?@%4|uID z;5?Kc?nIOuJ?*_V*K9;INnckr zTb6D?`wgR$^BfPj-BEnJI{sN;?7*8EJxc-c8rOoxtNTR6x1ej|;yoL^ zWJjF*=(09sUI|~+c*5c~vA=(PGF9cZ9PZ>R*42-hoGER?)MS$BFV8(k&6IqWtx~@1 z;Nhm}V=T^7X0|V|-m&CUy{5anc`9DLS8tZwhdfnQ*K=UBizIH4&xLKc)^9NRo^FDb z-l|ri>4@U%PgwMYXwRa1!bV>Qyd+NO^iE73I$*9khjVxheuS}D&flH{VFTrDSDzSK)H7z!gdUeHkN*%v+J!`>hfmaM9T=i5Mz#1 z{>4d0q5E4<`sF*FMmHK-Y;F`HLS9|2n^XqBR2em%XdRthSyk7W^XbmHsb(xz1Od?PZjYdZlRDavHgYloq#Ud#zoa)h=mQ z+A|Od+>%cwnI# z`o!PK{pbl0d{|7Vt}w7xB5#jQ)}!8M4;RES18o5U9HaDc5`= zWFK+CZaMUp(v;4Z7)#ZSt)E11L=_sb9yXkNFERRL&?r1U>h5Lku5uBrym0&DBbY1Z z0w;6}MNRBRI%BVl=cRZyus1Gp1T3fL487PdnY~GURkXU6JMFl&FZVTrFEUJ=8r)#? zHhCaZ7nRRH+Kfnwx@R9%d|Mei_awKuu#M&OY+0eVJvKOZufa90RaMH|HBHZZ%;^N% z9aWVp4foB=Ig)$C@#T}%Q z$q-KcVzzhLZ9HT|TBBPq+GL`Bc>e>Bq5Zcoh3%QH1Nj9RR1PX%XC1UGT1yt)wE1nh z`I5TX4&0xK+=31!z0;!*Hq5_WJ)a;Y?=K(>90C}MPb$Lww6pW`zlP(-c$ashzG?=w zr5#U>Fm7Gk+n@U&aK7X;yVVlY1(pk8QQRgO&xc(#SlWs=c6;+;qM;V2aG&DFM-DA5 z@buj!L*|9Ghp$9G3Gbd2FkJeo)baIsdGp9)ZuF|(fTPJ3&r}I(y%P*s>c!V3thmnW zd+?QeosGHgDJVF=8r(DXo_F+4x6%3H*$ZIk9ck_69%a#>s>+=L@2aNcBzGSxvsOv zh}9?BZb{@22T$E$PurH)*Ey219=b+cZaC~yFQdjmsj85vG{Gp#J=L$$GFg|LZFNtq zbn(QsHd70q7gXVo1e!`u_K4&>T$3L6Sl<_P+KbFLz~jUO7_CRX932f;>|6ctel)+3 z+kgJVyZz@)qxC!ywae4SIl`>PThO8I8HLR7%1JTXx3rGvir6#$@@`z4zF!cz_7mt0R;$>x`sSVa<@vCZ`OcS)6=2!3+RKOymlpo8enZRlMUQ#XzAwDK7Y;Q~-rB`! zvZ>&A-6LsAr(~nwqwOF!=Mg=uWtdFlZvLW_Nxd#fS_MZ5eAs@u)z|X3bnQ&-uot+z z*Gn(eQm2`PcGpzu2B~p|tTd>#00xZNp(hGC;xm@U6UJ}mM*d?c0yx38c(3P3zu+U#BW2g4ICOpUO#cK!O z50vGps;=}W9Sz$`eAv=si!4bmkkvQbx^JH#7kOVu>)|Pf+&ZD-9IkSn3+dVR^4uIp z^jyr+s%C|xGVM%8mXs16jmY!eSq)9sx-+sK>HZdVJH~Kl-^k&So}Imlw^4J^JbgTu)hi`iHf^q5hqbsvvdiS?@21-p?=-LvGEB<{jl*` z$oSDROV+a&(N)x_@SVEFAvE`beXKMV*2A3f_dCm?B5oZjX_TQ(Z}s#&k1?>Vdc>jO zH1ctGz@kM7Ra~F2CSB|<^)$VTqVjGg2jIw;5?X(h)+7g6S*_^H+0(xm-?gX$!CVnkEb7)7p* zxv7;|Y-0Q#DEeZ6?oriYp;!S)cx%jS@Om8Wy_u4nNNb~2%JyNVN@d8d_wc-a zr0Uz56oWM7q|;yXBUp+^#;%!Xr7cIwaR;{5Oy4=HJMQPI?X;g>g#RA%>AvI+7wYn^ z2$xk5$Kb1CX0Xyq2o!q1``!ntKHa1x#Ic(`3Yb`_J5N@!WvtK{UeeUCTfbKA zP+9z}a*6XZuJj{yUOwc+5%0oalD zU0zEG6?Jc!LcccN-zPLdee0r?3(au1^n^`PeR@xI%hS=kN#>k_i^jEUf>%O$nE>qTCNH%BHSv~|lDp1&nWeCyrSR4XOzD}ss~w(iptMrOi)LeW@^eiai;L>! zJk&NWXMF!3#@;fjt#Ioa#jUs%cXxM}1b4UM?(Xgq+$rwvPI0Hjp*RF9E~QYkH|IU? z9rycpzkeCY9vR8rOXgf_uJtU|u-bna+Q#0${~*OK$~c8G;~)iKCbFYFyXY=)9p5Jm z`hGia`k3B;hqo|qDu(-B_%q1s{R{IWHy4pY*$?hUA%PrbTg^SU`s}sdwtV~D*V^Um zS+pXRd0zAhU}B=CTT~}^Z=Q84Xw6NSUz1XET_Evgv!vmZ^s6)7Y6$s5+bpp?%ChMw z&yOw_|8_q&N`qsQUu|s$zVm!xf4&x0m_%s=NjLFQJpJM!qG7P0O?^bq0~pZ8o;Z4t z%H?0Oyys088qR38J6;QLMnczQ(0gz09Q}hrO8y6hcKKR?kuK!%ft~|2ClhcB;8S%$Koh1OooK8%IxE-ub(nlAL@^Bq@*tvH0L zw~UMA{Ljf%iFNpNZYZa6A9kV^8)pt6RBpqIGzzGi-_^hY+Tt{j|zg4v$0|xKf!MV>F6P=s9okf-0FLI&_t0wQ3kCwPOA=AEi4>nj%G6u6{;rw2tQpCJaYf zE|()J6o+_4XAoCBLujBcop;y~d}wpn0G9oChO${oF3;gNyzlI*>}CE%#V+P6#d^7t z)G9uZf;3Abfl2b%#^m6q^kAdwq*}2S(KPRWP`>_L>@#?fenT=2i)s`ck|4To#vMHm z)lkF=IMh!uEs3ZQl3Tt+*}kd#%0Ejk(Wj*lZl)TZN5LM7!xaT4^88VR zX^9L&dFYi;bkP2irP}i`;(Eg4=+k%i4|eJ|la?jusPy9i$OZ{8q+3cejB93r9D432 zDN+$PmZ=*5HU0H&vHTxIX`7azhaRZq=F8Itoi;f(@s2aXf+VU-htggNC7TPbG8a>S za1=jJ-=GCP;QQ973y35hQqE(Nr{Q{wQN4l5y#sYlfJ@><=+wd&sp|qyhYy&48^(%i zi^}Bp7z^2di2l`4T@a7Lpv}>{H5Dleng8dS{Qq1t)l^v)Ha|m!`cnRP;Ac}#VG`i&=&_Ybz1K&NLyg7AqVK(r4NNX+@*Jlp7mQEk^8F> zvQ7^0Cnh5(F+cn%dJ_rxuciP7K^#h@>c4t||Kl|@H01q%P!g6R1eif@mSZ7JO=KH? z#k4~y0J{wnvR3iYTnNxSad{cHH@lHqGeItD5#G|Nw&(s;p`)_A1}07 zpK&Qec^(pZ$9voR3|sDxpuQ67Bzipi$Nw5PmSdW+l_Wu;MsB}xUvkUfXOSfq32Wf~ z2)|&AX-q5t{w;tTWNL+}mFa zoEXhbaT;lqh%vKkmVZ!vJ1sRoYR8@;j0l=28CB56RlhL_C9o*z$j)($Y~Ju(YImyz znNTp_n^}d8PK=af-@{jaVS3k!b;d17CYdlz5X3#8R;f1&Pz_d$8d5Dm$QU7#;_G6;XCML{@&Ta%UH&RV{ltiHLy~B z7~AP|E@_mu>CTvf3te+QWNMFY3cC5_f*o;zH{lj`4rMW<@aB6fAlBcKhb_E<6VE~M z;=hUzLnuhy(>6}48c6;B(OSu8ahkO!Hs#q9cxo$MssXd*oy=tnI z5xSs8$1qT%QJ@Bwx*E>evhznXr;x|>p^|;rgNkUkUXsL856bOZg#I;0eG5O#5WePa zc_Ikydxx|4L=83*1ZqfjgRXZ=Rh_R$rtCrG;TZMitX09L*D9Q>2#CVDC=#XHZk)8BX!xB+ zscS>|8fVCOFNY{r$Pizc5{nZpdsThue}h{Ff)GEHwA0yc0SvN2E5OV^RaFF4P z<%Q_LGFFQqUG#G^_!i(ituju*=O6Yph@hT%Joyf%2H zRRB#N_KiIT4}dB>++-5N;hTuBel-144NCMbd>FMQxE1A%LKj!WKQru2wA!Kkb#({? zPMHaizby(}hRz7D;s99Ov%-%cQ$egBnw*U2)Q(Q?Z#$fCOl?6hDsHNRP##xv37i-H z?n&DQPL8=EgXp$v7ruLayaWvlvcV(P#xpX%XfcuxoJ|4p1~1eT5A|TINU-xmkl(=mIZ%HP;sl@u0)-;lLWzv!*|;!`{$f!VAqIe+QLn=#KHtq<3GG zhEkUm$L?UcX`)zmC?QKi7~d07Uyhv$#9!UKa9(haX;?4QFEl#sKe~}HvVECrakhme z-@<%XO}Dcu1t+v=T6XRKh-AXkNb=VN-;>>)?o_7UtGW78t;l|)Z)Z(6Q_nMOiD1>Ez zbuntR!K`YN%Y$c$cLkd~98VGp7SgfRE049W=b|`Z>9R)VQ%Fyj*vVagxg}JV+y|of zZSK<2!v3;>vX|noD)pbdP3uswQW2BZv*_>x6op)_Lh8yg5dONGb>2J&!O6uJ`NOEfozbyR1r~vfIhkwM6v3YYp6)2=~+<{F4a{Y8t6Yf^_u+o!* zjp{>|W&q6`iboQ$KW*c31Z~DrgJqds&^xJ3532-PWOLY=fCtxc&#W(N1SN+nrFl)~ zg#=~q#4o<)#Vh_o6M`9BWEA_e=JMdcGS%4QB~i*#Xnp)xGLi0B)e;(p zi6ZmQv8?-et^(qG*?^>)^AQ8B$s&ZR;>5ER!wbZc0~(tRuWOz$AHM6-BL;r_{nbF;V?@0s2|eU-Hnj|u1qyHhADXSbpzIm ztgAoHD)F{@&R_C!xI6TDP0%-#*Wl?fFi29gph8@q`M~Uqgj4SWbj&~#nQE}qK?ds2 z>T;{^ysyy(;K7lI9d<>qt(=rYjg6}t{XNbOQFn2qVU`m4Stsj-&`!-6)!!jxg{NOa zW3Ph?h8BJIj<&+U`JdGFa{HQ@g4U^W^h*txCD(vqCGZbQWp#c0Wfa5-UgoqBj!ncRjhzwoeqpCViCGoX6$^jiK1Z!wj@)A zr)WtO)$}X+?%><^5F_HQrm^rG*~Ua~rn2t17mMCleLQNCDbT?Xw)Jy*qCs zCWRr^n}3C6oGC|1)?X_1qGP1Wt79x7`#YlNEW^4EmyrT%H7d5-H_C-f zm4fGK3JIK;Zgh9;lmW;D?afSic&LK;G<&H>q%X5n@<;(A`ZfE58`_Gr~ua9NDr zEav^SFq*qqKS7l-4M{R)FnZ~z+hNy~PHrQ@&>1?SL#5BXn^S$?ap8U8$^C zIMSgSP|k|S)E=V>x%0YV2tL~1-wsgq2jY!nyKO*<6$ll4jELFkjAZR>sc+F~Af(IH znpLPX14bwR|d(>Y3HZhzCy?(mBJnZy4JMdXy{6Eqxf8@^|!Z zZ$E2EUUj=5H(eTb=ve)HI1SS(q91j#@vE|HSLCalgLE?VGQ(pTy?DSqnexyw#rt+K zn#BIFdyHgLHUk+xUKLrX(^kHHWO}K$xx#uV1VnjKrbcN;n?|hJR;Vp@GtGFwk;wO5 z#anJ!dD~HIzkm05Cto}~Kc^kD9LR6}jHwl6HK!BNu8Z|$Lyjg|(kPZkw0L>>np!bF z@pIR~OVw{keCC(}eU+0FJ%ncCa-O_|u$=Rhk>WcG7< zSksog)qnQoi4g#JsAxdr2ImwJ)iJVpxyDbW?OCjs3J6kFVO8qhua6Dsy3%R(k*8;~ zpJ*IO2_Vl?60sd?Ep6!xE=8f&Sgx9SZ`5duTE2a5&O8Mra?aPFK;X}3mw_Nd?a@$0 z9J!zIFapO0y2v7IWQ@36LV4ln=8*w$WhfA2a_ei@5rEDk$1qyX%CmE9i%y3$%^YCyLOMXMmpEM%=ZW^_E<>T3ap;Is(?0y+|IMC!At zKh!^;{1gpDvDb9w%JBE)FX!<{FuEJKFx0OIRyu57U&3hbfP0%9WPGSqeRa|~qeed? z)HN2Z?o8{#A;{qF3>#n{vnf54h!__=xTLZW%+@z76caa$TGdWMoM5`7$1CFJD}TxJ z(O7!*8lf|Hlt#3Q%zN3WhaL&x1k@Zr1LfXlFPZ0XfjV?Jv-)eT9D_>av-E17Mw!!J zXVqYiKC&CCh9xT}ZfxFAW@)3k)T^h@06fH40WQhZD$J(Xu%OZCc&iGGs6M1KeRC(aesnnZlx(W-uSwZh>j8f2Mj9~n zkbBu|9j1RtlEgk%Lml>4$etr#=V%`aW@|=P*sgqe9RhrOCz5_Z>(MlkiQ$;c$ve46 z3zpIvt6~C-Ij-?lOMUe?t`H)L$N_qwM;N$~Iv(Jr4*7kuTONs)X@4-o2j{Z-B#hv1 z(xgzW6*4$4B~8{4?LNtI68qR!E+|`n9l4)%HPzJAVJ@yx+t8^r>VsX4k55Qs^tep2 zZd&zOs+uGjf;K}%fu7NtQrvK6-dm3qOCQ$&>ItZWA{XPQuN^RZ5>b^&x=*!e^0VzQ zuIj@&8q%<-aNeE`E7}3xwcpWfkwgc2;!%U&LPmo6HosW>@qIN(cFPdqunhGDSq0ol zF#uI!eo)_;3K7+uw{x?Vn}yGUyDScz_d?3K^00>X4R(lBxB&0==>1vQ%(aZri2bG$ zhuVF4;}k#2`YJ3F&%AgwgeTzZPqJ^T*E3;3c%=4Zh!}Ik20?I%$}1AD^vYgPL4W~O z+uJI#A2)8}*`w@cH-GlaL&L?y@)DOj0yrBm>V3SF+^a3kq|kl2r9K(t*=$xVFA*ve zSVL+jcHTn@@L1E2-^ltljTtN*hp!O(%^Io@RjdUW(^=)3#+S1Bt~InMYRD3A2{60l zGg@>-YKrQ_+D5rz;w7{fqa+cVBO*k5vo=q8<9-Fwq#w|4XJr#Ul5N|$Wt*L$$E*pX zGxZ2OX4h)SqC)b#LxP#1MK0cVdk9u8Awe2y?-f@bCMp0{HQp)=DnZ|u?@L1xO4`za z?OmSFiW}no8R&YarhIX|xdzNhy3s86b@GM57_8=BBO_d+VHXm&Y-QNkgJrt3IwBg(O%VehlITpQJe&6*@ zptdy+)J8$%ay2Si7s_O)+A4oZgDUUfoEDygN2Xgi_Qb~7Urv#zwDSpR!0rdfM2q^rwHfC5VFeZ z^%-hqq5xJBhHQqn&mRSW+U63nhzK`lNgq>r5!pyNjyvQvfcl zURHOax0Uf1c4b$-x>Fd-;x3C9sRGe+ulVNLR4L9&^1t()-md?kMh`Au(@XYUts|{o z6ojNH%h2I0r1KQS#A-mYs7a}aoDnQ0%+?2!qrXI%d04bpRatz*%QB@W zMiT3pkOa85;(sbC)5VNT^x)h;!cdF61!ZQm3|1kug<^LUt%j_!aM1VMVzMkFwFd-E z0AZq?J6u`p&3QXld|5nJ_zZHVyC?t2nf?I}b?hZIj4jB|!DPX1w1M{P$eJMH5Ww`{ z$_i0dKJJw;KuF^rHfMU}){3tEZwu*89|92SB@VCTFXa162=A;&DOVO_VB<;3Nr2kL zx%NqS{s2^$0vjHGONXUa7$uoxPT|Ry0+vOhGL+*Y{jy@i>S2xn;ede`v;ABJh3Ld` zwYMNWkfSn zbQN3c%qbeInEivAE!~W2G!WV1uG=fmNw{)zqT5pYEv5g}Ai(Y+r}Oy34rI&U*NV+p z7A@eE8IJFyv~aNfz{TO1qvcBfjzBts^ryci7v2K<*0ui=doV?F+BSSf zs9cBo)|+%fK>CF3lFE&fC^YTBa&&2G) zO1(7OYZ_YY*{V#s$s2IX8#m{wUX#;+kmu6woo9PyFWblG{tCJc!w1R36ZAKit%f;s zzD;LvhZ+@@mLg#=bl%_@oiE@^`2gj@Vl*_Nz+;SdNs6_H&NVd}Tp6z**@*BwVrU8GalKBGpjOd{<^F;36eXzN3D26Tc%rC2{w;G!BW+t_gZW7PFh= z4Z8U#JuD2#+x!O=BkGeSEhC{Hw+}Ub$`nkxoj}M}Q%GEM9M2q?R$HO4LZs2%cKl^b zxT4qL)P?PgDtebjD>02Ilw@i|DY}L#M)Q}M`m?|3Jksr*6)Ys&U?thzfDTugCFsyD1icEx>p!L}kZZ1p=;m@0rO-Fl0XxRn9>3-E-| zda)v3Q5f}3a+KKgSaimayxl10h||xnI+j<&cBgxzCnRLonXU7+F9)2(2*<;;T75fF zb-(t-C#2CxKltY}fJN*IV~#v~i`7rpGjD?NIM))(&o5G&dJAwf$?o38aija$Q2DnK zeYhlg)*=o^W6sJQ>AT&dayPfpBtwt3_L^!|QYMqYI`&oIuo>H42qDpa4%DIAiU7_S ziHFXe*QbiYGrT&3sgd{t!r_u8z~P(Rhmq+9z);EMAvAuy_WZr{HI{DZ3Q zJ}o-w96Xpe2~ai=IkH@x$cti5dh*V9=uWpD?6GP4wtz3Ft}bacKS->y|EfJk495b# zaAP21m<1@TkD>c^buJ* z2J%A3F|Za0AXP>I{AKd0M9Y9ljPnac8`s{lRJn(UpTIh710^m^EfM=F&ZRW1_dMxJ z%R@Eyrh&*p>RLfut|H0#Iv*RT`Zl^u67HwF?|kc@Dwf@QOhzxKfY!vRg5hQk9lMI6 zKMRma`%kpAjG2(;2T|@)W`;0uvzFsH4)hp za5@H9ecL0()ORY=8>UcmeemL@u%I^S^aYENwdg<~`b0Ti?5*eyk9ePpttnSt&Axx- z%ZB^hT(Q3|B-XQ7gUT_3^ssASB-5*GMa>tJZ&Lo2Qe}z+3clPzsOq`xj5`G=wQZ*v zO7n6~6!+I(lT_y&iYatPmqL5uGP?JWL#OAT3cBZF?RghPwd?% zXDOL9V`SQF#c!4GY(M4ggu0<~YX=(dy z45slRPP7Kf4Qs=362hMU%CmYbODfPEkQC@^H~u!%(7z;8KJuqjQ2oUu(cm}!8LHa0 zN}4bWle&Fcdmyst4z}@47u+1CT#FQ3lGKZ6io+z1funbpVu!t$Q8Gy!D>OxLwioB) zimmZEdJGrdtQb*?UFYwshJHhzTp~i21MhT6F=~xTAq(rhPR6k{-A=JUdxhutR=ILS zHD!$A|HJmiy0YC_T&3!-=Om!DwC=`#Oi#;o1q>9VNelKw_QNk_EX`~rDxXf;!OV=k zL!}Bky!_NR#s7^b6q-M0W+L7Y%l8jz%+QuK@El02H}deC@E4OlNb4|`->K3nZ|T6R z1P&YCE(dP8%x9ydxtY`HDSflV)+nucY6@9sfI#H<$F6pN6V0tOTB*&6tTC^~YRTA| zQJ5Q_5q^-t17}J@kU@(}=Le;s;ds~i;)%Z06;n3R;n?l(g<>&_ zUnCIiM1O8R?>U16ogbGeljt<20>>Eap-KOCY}quo=vBJb>wHG!LcB0%K8w}ns9z$3 zg6Rd(AA@A&8lM8Z7rmc2+oUTF(R)b^Vz>*@PuVvPGg?B}#5rgU7wfRD`Y)WE`65!Y zcIo|UapC8>f?1ywsYzEe&Km$I`Ysvidamk^u7iaB%n_0dLuCvNlwL9Bdnr3;8_s30H2Z|F}L1e{aaZ zSEoqb71$uk3*}+i`B`NBCLGONQzm2`@Y7HwOJY=!*{4AC;RMM9z@|Y>Rn5B1Yfj+I z>S)y_i!ygcJpcv!rv?A8ZAx}MjxKBkFnp-ZCY7elv_Tg0^RZ7+-(25BR1@&SHk6Dq zCYYt?i)VX#{HH80>u5T_E#OO>#m>TO<*O@Il(es;#s?njP>L_AqQhSw2e`+`O!g4i zx-%xm;TR{^E`F+A1cOD>-*|z5!G_4JwNJ!J)XHr5KwPS#rdYmjUaGCv9-UEq&0J%j z@U-i=eF|=~b==Dr#2jcG301!#6Z8%y!ramoqg#OZlj1*p!jEw7%i83#HEPlOS>2TI zxhS^(?$~f!x<_mats~Djc`*jSnZ>HYE0FH;^U87A4(?ZGS(;!QE^nFX4q%}V8$BE1 zKJS-$mm#rykGD>Tzuqq73Z^U6^3vu!8aN$#AsS*QHn4mdMYz;dNtCTJ8irO`e9hE; z#3i}qL#!pQ^ibiPZngSEg_#tFtpFE5yif4<1;a-!Rduo8YXH)SGGVO%T)?}_Z$qAP z*4IBIu=s~J>lJiS@p6#r2pEZ%K>k90sfOtZ>pjxTvo~6E3`;%kiL`RjsRsmrccv?p zO^|1qE`X1N#rkDIV+((_8o2k}@3qvFRWj#bbL6h#USxL?Y$AwZxY; zv?G07-#Cn0yD->c~9v*dQi&!#dB-!zetUwA2Ak#_!e zk&M%@KgOZTAH^YK_#D-yOd!urk-i z(9&3hyf+iDl_1p3Z@@V{*PLtmQFaNlUgbEH(!G(f6VR#isk¬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
F5C39XCkAg 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 iVA-!+pM~v_{o_K=2mTMP5?3Ms literal 0 HcmV?d00001 diff --git a/RIGS/templates/RIGS/event_print.xml b/RIGS/templates/RIGS/event_print.xml index 1a586705..890fc9bc 100644 --- a/RIGS/templates/RIGS/event_print.xml +++ b/RIGS/templates/RIGS/event_print.xml @@ -65,23 +65,26 @@ -