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 diff --git a/RIGS/ical.py b/RIGS/ical.py new file mode 100644 index 00000000..944d03a9 --- /dev/null +++ b/RIGS/ical.py @@ -0,0 +1,125 @@ +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 + +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): + #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).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: ' + + if not item.is_rig: + title += 'NON-RIG: ' + + 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 + + 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): + # 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 + + 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): + # 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' + 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+'\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 + # base_url = "https://pyrigs.nottinghamtec.co.uk" + return item.get_absolute_url() + + # def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though + # return '' + + def item_updated(self, item): # some ical clients will display this + return item.last_edited_at + + 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/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, + ), + ] diff --git a/RIGS/models.py b/RIGS/models.py index a9b85f03..641aeab9 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -7,6 +7,9 @@ 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 django.core.urlresolvers import reverse_lazy from decimal import Decimal @@ -14,6 +17,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=True,editable=False, null=True) + + @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 +37,6 @@ class Profile(AbstractUser): def name(self): return self.get_full_name() + ' "' + self.initials + '"' - class RevisionMixin(object): @property def last_edited_at(self): @@ -295,6 +305,9 @@ class Event(models.Model, RevisionMixin): objects = EventManager() + def get_absolute_url(self): + return reverse_lazy('event_detail', kwargs={'pk': self.pk}) + def __str__(self): return str(self.pk) + ": " + self.name diff --git a/RIGS/templates/RIGS/profile_detail.html b/RIGS/templates/RIGS/profile_detail.html index 174e3f3b..9e236ccc 100644 --- a/RIGS/templates/RIGS/profile_detail.html +++ b/RIGS/templates/RIGS/profile_detail.html @@ -22,10 +22,7 @@ {% endif %} - - - -
+
First Name
{{object.first_name}}
@@ -48,10 +45,44 @@
Phone
{{object.phone}}
+ {% if object.pk == user.pk %} + + + +

Personal iCal Details

+ +
+
API Key
+
+ {% if user.api_key %} + {{user.api_key}} + {% else %} + No API Key Generated + {% endif %} +
+ +
Calendar URL
+
+ {% if user.api_key %} +
http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}
+ {% else %} +
No API Key Generated
+ {% endif %} +
+
+ + {% endif %}
-
+ +
+
{% endblock %} \ No newline at end of file diff --git a/RIGS/urls.py b/RIGS/urls.py index aa9023c3..9862ee72 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -1,9 +1,10 @@ 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 +from PyRIGS.decorators import api_key_required urlpatterns = patterns('', # Examples: @@ -114,6 +115,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 - 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 50366f76..bd48cb92 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -355,4 +355,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') 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