From 46434977fb4b3f186f0044417f878cb59e890f26 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 5 Apr 2016 04:18:19 +0100 Subject: [PATCH 01/67] Created merge admin action for Person, Venue and Organisation models. Added template. --- RIGS/admin.py | 46 +++++++++++++++++-- .../templates/RIGS/admin_associate_merge.html | 21 +++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 RIGS/templates/RIGS/admin_associate_merge.html diff --git a/RIGS/admin.py b/RIGS/admin.py index 18dc554e..a72c4705 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -4,16 +4,20 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ import reversion +from django.contrib.admin import helpers +from django.template.response import TemplateResponse +from django.contrib import messages +from django.db import transaction +from django.core.exceptions import ObjectDoesNotExist + # Register your models here. -admin.site.register(models.Person, reversion.VersionAdmin) -admin.site.register(models.Organisation, reversion.VersionAdmin) admin.site.register(models.VatRate, reversion.VersionAdmin) -admin.site.register(models.Venue, reversion.VersionAdmin) admin.site.register(models.Event, reversion.VersionAdmin) admin.site.register(models.EventItem, reversion.VersionAdmin) admin.site.register(models.Invoice) admin.site.register(models.Payment) +@admin.register(models.Profile) class ProfileAdmin(UserAdmin): fieldsets = ( (None, {'fields': ('username', 'password')}), @@ -33,4 +37,38 @@ class ProfileAdmin(UserAdmin): form = forms.ProfileChangeForm add_form = forms.ProfileCreationForm -admin.site.register(models.Profile, ProfileAdmin) +@admin.register(models.Person, models.Organisation, models.Venue) +class AssociateAdmin(reversion.VersionAdmin): + list_display = ('id', 'name','number_of_events') + search_fields = ['id','name'] + + actions = ['merge'] + + def number_of_events(self,obj): + return obj.latest_events.count() + + def merge(self, request, queryset): + if request.POST.get('post'): # Has the user confirmed which is the master record? + try: + masterObjectPk = request.POST.get('master') + masterObject = queryset.get(pk = masterObjectPk) + except ObjectDoesNotExist: + self.message_user(request, "An error occured. Did you select a 'master' record?",level=messages.ERROR) + return + + with transaction.atomic(), reversion.create_revision(): + for obj in queryset.exclude(pk = masterObjectPk): + events = obj.event_set.all() + for event in events: + masterObject.event_set.add(event) + obj.delete() + + self.message_user(request, "Objects successfully merged.") + return + else: # Present the confirmation screen + context = { + 'title': _("Are you sure?"), + 'queryset': queryset, + 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + } + return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context, current_app=self.admin_site.name) \ No newline at end of file diff --git a/RIGS/templates/RIGS/admin_associate_merge.html b/RIGS/templates/RIGS/admin_associate_merge.html new file mode 100644 index 00000000..e30b2e55 --- /dev/null +++ b/RIGS/templates/RIGS/admin_associate_merge.html @@ -0,0 +1,21 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n %} + +{% block content %} +
{% csrf_token %} +

The following objects will be merged. Please select the 'master' record which you would like to keep. Other records will have associated events moved to the 'master' copy, and then will be deleted.

+ + {% for item in queryset %} + {{item.pk}} | {{item}}
+ {% endfor %} + +
+ {% for obj in queryset %} + + {% endfor %} + + + +
+
+{% endblock %} \ No newline at end of file From 33ce4b622d3a8e034178f0bee8a10d0f72caea3d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 5 Apr 2016 04:18:53 +0100 Subject: [PATCH 02/67] Fixed bug with versioning interface when related objects are deleted --- RIGS/versioning.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/RIGS/versioning.py b/RIGS/versioning.py index 789fc5d7..c537de87 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -92,9 +92,16 @@ def model_compare(oldObj, newObj, excluded_keys=[]): if name in excluded_keys: continue # if we're excluding this field, skip over it - oldValue = getattr(oldObj, name, None) - newValue = getattr(newObj, name, None) + try: + oldValue = getattr(oldObj, name, None) + except ObjectDoesNotExist: + oldValue = None + try: + newValue = getattr(newObj, name, None) + except ObjectDoesNotExist: + newValue = None + try: bothBlank = (not oldValue) and (not newValue) if oldValue != newValue and not bothBlank: From ca6cddb3924bd185532a716ebb12aa9b79f951b7 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 5 Apr 2016 11:50:34 +0100 Subject: [PATCH 03/67] Add comments display to versioning history (because why not). Maybe in future we could have a box people can type in before they save changes to an event... But that's a separate project --- RIGS/admin.py | 3 ++- RIGS/templates/RIGS/activity_feed_data.html | 3 +++ RIGS/templates/RIGS/activity_table.html | 2 ++ RIGS/templates/RIGS/version_history.html | 4 ++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/RIGS/admin.py b/RIGS/admin.py index a72c4705..d8583e23 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -62,7 +62,8 @@ class AssociateAdmin(reversion.VersionAdmin): for event in events: masterObject.event_set.add(event) obj.delete() - + reversion.set_comment('Merging Objects') + self.message_user(request, "Objects successfully merged.") return else: # Present the confirmation screen diff --git a/RIGS/templates/RIGS/activity_feed_data.html b/RIGS/templates/RIGS/activity_feed_data.html index fc9af87e..e99baf8e 100644 --- a/RIGS/templates/RIGS/activity_feed_data.html +++ b/RIGS/templates/RIGS/activity_feed_data.html @@ -40,6 +40,9 @@ {% endif %} {% include 'RIGS/object_button.html' with object=version.new %} + {% if version.revision.comment %} + ({{ version.revision.comment }}) + {% endif %}

diff --git a/RIGS/templates/RIGS/activity_table.html b/RIGS/templates/RIGS/activity_table.html index bf625e44..1d491663 100644 --- a/RIGS/templates/RIGS/activity_table.html +++ b/RIGS/templates/RIGS/activity_table.html @@ -59,6 +59,7 @@ Version ID User Changes + Comment @@ -75,6 +76,7 @@ {% else %} {% include 'RIGS/version_changes.html' %} {% endif %} + {{ version.revision.comment }} {% endfor %} diff --git a/RIGS/templates/RIGS/version_history.html b/RIGS/templates/RIGS/version_history.html index 163a1ac5..924ea148 100644 --- a/RIGS/templates/RIGS/version_history.html +++ b/RIGS/templates/RIGS/version_history.html @@ -35,6 +35,7 @@ Version ID User Changes + Comment @@ -51,6 +52,9 @@ {% include 'RIGS/version_changes.html' %} {% endif %} + + {{ version.revision.comment }} + {% endif %} {% endfor %} From 03ca65602fd2e41a6556473820fa493c774e97c6 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 5 Apr 2016 12:08:19 +0100 Subject: [PATCH 04/67] Allow sorting by number of events --- RIGS/admin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RIGS/admin.py b/RIGS/admin.py index d8583e23..9bfcb3eb 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -9,6 +9,7 @@ from django.template.response import TemplateResponse from django.contrib import messages from django.db import transaction from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Count # Register your models here. admin.site.register(models.VatRate, reversion.VersionAdmin) @@ -44,9 +45,14 @@ class AssociateAdmin(reversion.VersionAdmin): actions = ['merge'] + def get_queryset(self, request): + return super(AssociateAdmin, self).get_queryset(request).annotate(event_count = Count('event')) + def number_of_events(self,obj): return obj.latest_events.count() + number_of_events.admin_order_field = 'event_count' + def merge(self, request, queryset): if request.POST.get('post'): # Has the user confirmed which is the master record? try: From 99dfdcd253a094f3eb4c5613f600024a60ef72a6 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 5 Apr 2016 12:53:04 +0100 Subject: [PATCH 05/67] Make confirmation more useful --- RIGS/admin.py | 35 ++++++++++++++++--- .../templates/RIGS/admin_associate_merge.html | 25 +++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/RIGS/admin.py b/RIGS/admin.py index 9bfcb3eb..0dfafd7a 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -10,6 +10,7 @@ from django.contrib import messages from django.db import transaction from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count +from django.forms import ModelForm # Register your models here. admin.site.register(models.VatRate, reversion.VersionAdmin) @@ -38,19 +39,19 @@ class ProfileAdmin(UserAdmin): form = forms.ProfileChangeForm add_form = forms.ProfileCreationForm -@admin.register(models.Person, models.Organisation, models.Venue) class AssociateAdmin(reversion.VersionAdmin): list_display = ('id', 'name','number_of_events') search_fields = ['id','name'] - + list_display_links = ['id','name'] actions = ['merge'] + merge_fields = ['name'] + def get_queryset(self, request): return super(AssociateAdmin, self).get_queryset(request).annotate(event_count = Count('event')) def number_of_events(self,obj): return obj.latest_events.count() - number_of_events.admin_order_field = 'event_count' def merge(self, request, queryset): @@ -73,9 +74,35 @@ class AssociateAdmin(reversion.VersionAdmin): self.message_user(request, "Objects successfully merged.") return else: # Present the confirmation screen + + class TempForm(ModelForm): + class Meta: + model = queryset.model + fields = self.merge_fields + + forms = [] + for obj in queryset: + forms.append(TempForm(instance=obj)) + context = { 'title': _("Are you sure?"), 'queryset': queryset, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + 'forms': forms } - return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context, current_app=self.admin_site.name) \ No newline at end of file + return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context, current_app=self.admin_site.name) + +@admin.register(models.Person) +class PersonAdmin(AssociateAdmin): + list_display = ('id', 'name','phone','email','number_of_events') + merge_fields = ['name','phone','email','address','notes'] + +@admin.register(models.Venue) +class VenueAdmin(AssociateAdmin): + list_display = ('id', 'name','phone','email','number_of_events') + merge_fields = ['name','phone','email','address','notes','three_phase_available'] + +@admin.register(models.Organisation) +class OrganisationAdmin(AssociateAdmin): + list_display = ('id', 'name','phone','email','number_of_events') + merge_fields = ['name','phone','email','address','notes','union_account'] \ No newline at end of file diff --git a/RIGS/templates/RIGS/admin_associate_merge.html b/RIGS/templates/RIGS/admin_associate_merge.html index e30b2e55..2128725c 100644 --- a/RIGS/templates/RIGS/admin_associate_merge.html +++ b/RIGS/templates/RIGS/admin_associate_merge.html @@ -5,13 +5,32 @@
{% csrf_token %}

The following objects will be merged. Please select the 'master' record which you would like to keep. Other records will have associated events moved to the 'master' copy, and then will be deleted.

- {% for item in queryset %} - {{item.pk}} | {{item}}
+ + {% for form in forms %} + {% if forloop.first %} + + + + {% for field in form %} + + {% endfor %} + + {% endif %} + + + + + {% for field in form %} + + {% endfor %} + {% endfor %} +
ID {{ field.label }}
{{form.instance.pk}} {{ field.value }}
+
{% for obj in queryset %} - + {% endfor %} From 44ccead0a46daea308a8657815170867f24bb43d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 5 Apr 2016 15:32:13 +0100 Subject: [PATCH 06/67] Added merge tests I feel like it should be possible to abstract some of these so that they're not copied out for each model, but not sure how to go about it... --- RIGS/test_unit.py | 139 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 RIGS/test_unit.py diff --git a/RIGS/test_unit.py b/RIGS/test_unit.py new file mode 100644 index 00000000..b6fc8edd --- /dev/null +++ b/RIGS/test_unit.py @@ -0,0 +1,139 @@ +from django.core.urlresolvers import reverse +from django.test import TestCase +from datetime import date + +from RIGS import models +from django.core.exceptions import ObjectDoesNotExist + +class TestAdminMergeObjects(TestCase): + @classmethod + def setUpTestData(cls): + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True) + + cls.persons = { + 1: models.Person.objects.create(name="Person 1"), + 2: models.Person.objects.create(name="Person 2"), + 3: models.Person.objects.create(name="Person 3"), + } + + cls.organisations = { + 1: models.Organisation.objects.create(name="Organisation 1"), + 2: models.Organisation.objects.create(name="Organisation 2"), + 3: models.Organisation.objects.create(name="Organisation 3"), + } + + cls.venues = { + 1: models.Venue.objects.create(name="Venue 1"), + 2: models.Venue.objects.create(name="Venue 2"), + 3: models.Venue.objects.create(name="Venue 3"), + } + + cls.events = { + 1: models.Event.objects.create(name="TE E1", start_date=date.today(), person=cls.persons[1], organisation=cls.organisations[3], venue=cls.venues[2]), + 2: models.Event.objects.create(name="TE E2", start_date=date.today(), person=cls.persons[2], organisation=cls.organisations[2], venue=cls.venues[3]), + 3: models.Event.objects.create(name="TE E3", start_date=date.today(), person=cls.persons[3], organisation=cls.organisations[1], venue=cls.venues[1]), + 4: models.Event.objects.create(name="TE E4", start_date=date.today(), person=cls.persons[3], organisation=cls.organisations[3], venue=cls.venues[3]), + } + + def setUp(self): + self.profile.set_password('testuser') + self.profile.save() + self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + + def test_merge_confirmation(self): + change_url = reverse('admin:RIGS_venue_changelist') + data = { + 'action': 'merge', + '_selected_action': [unicode(val.pk) for key,val in self.venues.iteritems()] + + } + response = self.client.post(change_url, data, follow=True) + + self.assertContains(response, "The following objects will be merged") + for key,venue in self.venues.iteritems(): + self.assertContains(response, venue.name) + + def test_merge_no_master(self): + change_url = reverse('admin:RIGS_venue_changelist') + data = {'action': 'merge', + '_selected_action': [unicode(val.pk) for key,val in self.venues.iteritems()], + 'post':'yes', + } + response = self.client.post(change_url, data, follow=True) + + self.assertContains(response, "An error occured") + + def test_venue_merge(self): + change_url = reverse('admin:RIGS_venue_changelist') + + data = {'action': 'merge', + '_selected_action': [unicode(val.pk) for key,val in self.venues.iteritems()], + 'post':'yes', + 'master':self.venues[1].pk + } + + response = self.client.post(change_url, data, follow=True) + self.assertContains(response, "Objects successfully merged") + self.assertContains(response, self.venues[1].name) + + # Check the master copy still exists + self.assertTrue(models.Venue.objects.get(pk=self.venues[1].pk)) + + # Check the un-needed venues have been disposed of + self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[2].pk) + self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[3].pk) + + # Check the events have been moved to the master venue + for key,event in self.events.iteritems(): + updatedEvent = models.Event.objects.get(pk=event.pk) + self.assertEqual(updatedEvent.venue, self.venues[1]) + + def test_person_merge(self): + change_url = reverse('admin:RIGS_person_changelist') + + data = {'action': 'merge', + '_selected_action': [unicode(val.pk) for key,val in self.persons.iteritems()], + 'post':'yes', + 'master':self.persons[1].pk + } + + response = self.client.post(change_url, data, follow=True) + self.assertContains(response, "Objects successfully merged") + self.assertContains(response, self.persons[1].name) + + # Check the master copy still exists + self.assertTrue(models.Person.objects.get(pk=self.persons[1].pk)) + + # Check the un-needed people have been disposed of + self.assertRaises(ObjectDoesNotExist, models.Person.objects.get, pk=self.persons[2].pk) + self.assertRaises(ObjectDoesNotExist, models.Person.objects.get, pk=self.persons[3].pk) + + # Check the events have been moved to the master person + for key,event in self.events.iteritems(): + updatedEvent = models.Event.objects.get(pk=event.pk) + self.assertEqual(updatedEvent.person, self.persons[1]) + + def test_organisation_merge(self): + change_url = reverse('admin:RIGS_organisation_changelist') + + data = {'action': 'merge', + '_selected_action': [unicode(val.pk) for key,val in self.organisations.iteritems()], + 'post':'yes', + 'master':self.organisations[1].pk + } + + response = self.client.post(change_url, data, follow=True) + self.assertContains(response, "Objects successfully merged") + self.assertContains(response, self.organisations[1].name) + + # Check the master copy still exists + self.assertTrue(models.Organisation.objects.get(pk=self.organisations[1].pk)) + + # Check the un-needed organisations have been disposed of + self.assertRaises(ObjectDoesNotExist, models.Organisation.objects.get, pk=self.organisations[2].pk) + self.assertRaises(ObjectDoesNotExist, models.Organisation.objects.get, pk=self.organisations[3].pk) + + # Check the events have been moved to the master organisation + for key,event in self.events.iteritems(): + updatedEvent = models.Event.objects.get(pk=event.pk) + self.assertEqual(updatedEvent.organisation, self.organisations[1]) \ No newline at end of file From ebe08c3bf1dc7b3dd9411c0de691bee9a91468eb Mon Sep 17 00:00:00 2001 From: Tom Price Date: Wed, 6 Apr 2016 21:52:25 +0100 Subject: [PATCH 07/67] Update tests to check items that aren't selected aren't affected. PEP8 format the file --- RIGS/test_unit.py | 226 +++++++++++++++++++++++++--------------------- 1 file changed, 122 insertions(+), 104 deletions(-) diff --git a/RIGS/test_unit.py b/RIGS/test_unit.py index b6fc8edd..75fcca5a 100644 --- a/RIGS/test_unit.py +++ b/RIGS/test_unit.py @@ -5,135 +5,153 @@ from datetime import date from RIGS import models from django.core.exceptions import ObjectDoesNotExist + class TestAdminMergeObjects(TestCase): - @classmethod - def setUpTestData(cls): - cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True) + @classmethod + def setUpTestData(cls): + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, + is_active=True, is_staff=True) - cls.persons = { - 1: models.Person.objects.create(name="Person 1"), - 2: models.Person.objects.create(name="Person 2"), - 3: models.Person.objects.create(name="Person 3"), - } + cls.persons = { + 1: models.Person.objects.create(name="Person 1"), + 2: models.Person.objects.create(name="Person 2"), + 3: models.Person.objects.create(name="Person 3"), + } - cls.organisations = { - 1: models.Organisation.objects.create(name="Organisation 1"), - 2: models.Organisation.objects.create(name="Organisation 2"), - 3: models.Organisation.objects.create(name="Organisation 3"), - } + cls.organisations = { + 1: models.Organisation.objects.create(name="Organisation 1"), + 2: models.Organisation.objects.create(name="Organisation 2"), + 3: models.Organisation.objects.create(name="Organisation 3"), + } - cls.venues = { - 1: models.Venue.objects.create(name="Venue 1"), - 2: models.Venue.objects.create(name="Venue 2"), - 3: models.Venue.objects.create(name="Venue 3"), - } + cls.venues = { + 1: models.Venue.objects.create(name="Venue 1"), + 2: models.Venue.objects.create(name="Venue 2"), + 3: models.Venue.objects.create(name="Venue 3"), + } - cls.events = { - 1: models.Event.objects.create(name="TE E1", start_date=date.today(), person=cls.persons[1], organisation=cls.organisations[3], venue=cls.venues[2]), - 2: models.Event.objects.create(name="TE E2", start_date=date.today(), person=cls.persons[2], organisation=cls.organisations[2], venue=cls.venues[3]), - 3: models.Event.objects.create(name="TE E3", start_date=date.today(), person=cls.persons[3], organisation=cls.organisations[1], venue=cls.venues[1]), - 4: models.Event.objects.create(name="TE E4", start_date=date.today(), person=cls.persons[3], organisation=cls.organisations[3], venue=cls.venues[3]), - } + cls.events = { + 1: models.Event.objects.create(name="TE E1", start_date=date.today(), person=cls.persons[1], + organisation=cls.organisations[3], venue=cls.venues[2]), + 2: models.Event.objects.create(name="TE E2", start_date=date.today(), person=cls.persons[2], + organisation=cls.organisations[2], venue=cls.venues[3]), + 3: models.Event.objects.create(name="TE E3", start_date=date.today(), person=cls.persons[3], + organisation=cls.organisations[1], venue=cls.venues[1]), + 4: models.Event.objects.create(name="TE E4", start_date=date.today(), person=cls.persons[3], + organisation=cls.organisations[3], venue=cls.venues[3]), + } - def setUp(self): - self.profile.set_password('testuser') - self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + def setUp(self): + self.profile.set_password('testuser') + self.profile.save() + self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) - def test_merge_confirmation(self): - change_url = reverse('admin:RIGS_venue_changelist') - data = { - 'action': 'merge', - '_selected_action': [unicode(val.pk) for key,val in self.venues.iteritems()] + def test_merge_confirmation(self): + change_url = reverse('admin:RIGS_venue_changelist') + data = { + 'action': 'merge', + '_selected_action': [unicode(val.pk) for key, val in self.venues.iteritems()] - } - response = self.client.post(change_url, data, follow=True) + } + response = self.client.post(change_url, data, follow=True) - self.assertContains(response, "The following objects will be merged") - for key,venue in self.venues.iteritems(): - self.assertContains(response, venue.name) + self.assertContains(response, "The following objects will be merged") + for key, venue in self.venues.iteritems(): + self.assertContains(response, venue.name) - def test_merge_no_master(self): - change_url = reverse('admin:RIGS_venue_changelist') - data = {'action': 'merge', - '_selected_action': [unicode(val.pk) for key,val in self.venues.iteritems()], - 'post':'yes', - } - response = self.client.post(change_url, data, follow=True) + def test_merge_no_master(self): + change_url = reverse('admin:RIGS_venue_changelist') + data = {'action': 'merge', + '_selected_action': [unicode(val.pk) for key, val in self.venues.iteritems()], + 'post': 'yes', + } + response = self.client.post(change_url, data, follow=True) - self.assertContains(response, "An error occured") - - def test_venue_merge(self): - change_url = reverse('admin:RIGS_venue_changelist') + self.assertContains(response, "An error occured") - data = {'action': 'merge', - '_selected_action': [unicode(val.pk) for key,val in self.venues.iteritems()], - 'post':'yes', - 'master':self.venues[1].pk - } + def test_venue_merge(self): + change_url = reverse('admin:RIGS_venue_changelist') - response = self.client.post(change_url, data, follow=True) - self.assertContains(response, "Objects successfully merged") - self.assertContains(response, self.venues[1].name) + data = {'action': 'merge', + '_selected_action': [unicode(self.venues[1].pk), unicode(self.venues[2].pk)], + 'post': 'yes', + 'master': self.venues[1].pk + } - # Check the master copy still exists - self.assertTrue(models.Venue.objects.get(pk=self.venues[1].pk)) + response = self.client.post(change_url, data, follow=True) + self.assertContains(response, "Objects successfully merged") + self.assertContains(response, self.venues[1].name) - # Check the un-needed venues have been disposed of - self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[2].pk) - self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[3].pk) + # Check the master copy still exists + self.assertTrue(models.Venue.objects.get(pk=self.venues[1].pk)) - # Check the events have been moved to the master venue - for key,event in self.events.iteritems(): - updatedEvent = models.Event.objects.get(pk=event.pk) - self.assertEqual(updatedEvent.venue, self.venues[1]) + # Check the un-needed venue has been disposed of + self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[2].pk) - def test_person_merge(self): - change_url = reverse('admin:RIGS_person_changelist') + # Check the one we didn't delete is still there + self.assertEqual(models.Venue.objects.get(pk=self.venues[3].pk), self.venues[3]) - data = {'action': 'merge', - '_selected_action': [unicode(val.pk) for key,val in self.persons.iteritems()], - 'post':'yes', - 'master':self.persons[1].pk - } + # Check the events have been moved to the master venue + for key, event in self.events.iteritems(): + updatedEvent = models.Event.objects.get(pk=event.pk) + if event.venue == self.venues[3]: # The one we left in place + continue + self.assertEqual(updatedEvent.venue, self.venues[1]) - response = self.client.post(change_url, data, follow=True) - self.assertContains(response, "Objects successfully merged") - self.assertContains(response, self.persons[1].name) + def test_person_merge(self): + change_url = reverse('admin:RIGS_person_changelist') - # Check the master copy still exists - self.assertTrue(models.Person.objects.get(pk=self.persons[1].pk)) + data = {'action': 'merge', + '_selected_action': [unicode(self.persons[1].pk), unicode(self.persons[2].pk)], + 'post': 'yes', + 'master': self.persons[1].pk + } - # Check the un-needed people have been disposed of - self.assertRaises(ObjectDoesNotExist, models.Person.objects.get, pk=self.persons[2].pk) - self.assertRaises(ObjectDoesNotExist, models.Person.objects.get, pk=self.persons[3].pk) + response = self.client.post(change_url, data, follow=True) + self.assertContains(response, "Objects successfully merged") + self.assertContains(response, self.persons[1].name) - # Check the events have been moved to the master person - for key,event in self.events.iteritems(): - updatedEvent = models.Event.objects.get(pk=event.pk) - self.assertEqual(updatedEvent.person, self.persons[1]) + # Check the master copy still exists + self.assertTrue(models.Person.objects.get(pk=self.persons[1].pk)) - def test_organisation_merge(self): - change_url = reverse('admin:RIGS_organisation_changelist') + # Check the un-needed people have been disposed of + self.assertRaises(ObjectDoesNotExist, models.Person.objects.get, pk=self.persons[2].pk) - data = {'action': 'merge', - '_selected_action': [unicode(val.pk) for key,val in self.organisations.iteritems()], - 'post':'yes', - 'master':self.organisations[1].pk - } + # Check the one we didn't delete is still there + self.assertEqual(models.Person.objects.get(pk=self.persons[3].pk), self.persons[3]) - response = self.client.post(change_url, data, follow=True) - self.assertContains(response, "Objects successfully merged") - self.assertContains(response, self.organisations[1].name) + # Check the events have been moved to the master person + for key, event in self.events.iteritems(): + updatedEvent = models.Event.objects.get(pk=event.pk) + if event.person == self.persons[3]: # The one we left in place + continue + self.assertEqual(updatedEvent.person, self.persons[1]) - # Check the master copy still exists - self.assertTrue(models.Organisation.objects.get(pk=self.organisations[1].pk)) + def test_organisation_merge(self): + change_url = reverse('admin:RIGS_organisation_changelist') - # Check the un-needed organisations have been disposed of - self.assertRaises(ObjectDoesNotExist, models.Organisation.objects.get, pk=self.organisations[2].pk) - self.assertRaises(ObjectDoesNotExist, models.Organisation.objects.get, pk=self.organisations[3].pk) + data = {'action': 'merge', + '_selected_action': [unicode(self.organisations[1].pk), unicode(self.organisations[2].pk)], + 'post': 'yes', + 'master': self.organisations[1].pk + } - # Check the events have been moved to the master organisation - for key,event in self.events.iteritems(): - updatedEvent = models.Event.objects.get(pk=event.pk) - self.assertEqual(updatedEvent.organisation, self.organisations[1]) \ No newline at end of file + response = self.client.post(change_url, data, follow=True) + self.assertContains(response, "Objects successfully merged") + self.assertContains(response, self.organisations[1].name) + + # Check the master copy still exists + self.assertTrue(models.Organisation.objects.get(pk=self.organisations[1].pk)) + + # Check the un-needed organisations have been disposed of + self.assertRaises(ObjectDoesNotExist, models.Organisation.objects.get, pk=self.organisations[2].pk) + + # Check the one we didn't delete is still there + self.assertEqual(models.Organisation.objects.get(pk=self.organisations[3].pk), self.organisations[3]) + + # Check the events have been moved to the master organisation + for key, event in self.events.iteritems(): + updatedEvent = models.Event.objects.get(pk=event.pk) + if event.organisation == self.organisations[3]: # The one we left in place + continue + self.assertEqual(updatedEvent.organisation, self.organisations[1]) From 823db68a6a534e6f445f456c4893ff83cefaf742 Mon Sep 17 00:00:00 2001 From: Tom Price Date: Wed, 6 Apr 2016 21:53:38 +0100 Subject: [PATCH 08/67] PEP8 format files --- RIGS/admin.py | 49 ++++++++++-------- RIGS/versioning.py | 122 ++++++++++++++++++++++++--------------------- 2 files changed, 92 insertions(+), 79 deletions(-) diff --git a/RIGS/admin.py b/RIGS/admin.py index 0dfafd7a..a351aed0 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -6,7 +6,7 @@ import reversion from django.contrib.admin import helpers from django.template.response import TemplateResponse -from django.contrib import messages +from django.contrib import messages from django.db import transaction from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count @@ -19,16 +19,17 @@ admin.site.register(models.EventItem, reversion.VersionAdmin) admin.site.register(models.Invoice) admin.site.register(models.Payment) + @admin.register(models.Profile) class ProfileAdmin(UserAdmin): fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), { - 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}), + 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}), (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), (_('Important dates'), { - 'fields': ('last_login', 'date_joined')}), + 'fields': ('last_login', 'date_joined')}), ) add_fieldsets = ( (None, { @@ -39,41 +40,43 @@ class ProfileAdmin(UserAdmin): form = forms.ProfileChangeForm add_form = forms.ProfileCreationForm + class AssociateAdmin(reversion.VersionAdmin): - list_display = ('id', 'name','number_of_events') - search_fields = ['id','name'] - list_display_links = ['id','name'] + list_display = ('id', 'name', 'number_of_events') + search_fields = ['id', 'name'] + list_display_links = ['id', 'name'] actions = ['merge'] merge_fields = ['name'] def get_queryset(self, request): - return super(AssociateAdmin, self).get_queryset(request).annotate(event_count = Count('event')) + return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event')) - def number_of_events(self,obj): + def number_of_events(self, obj): return obj.latest_events.count() + number_of_events.admin_order_field = 'event_count' def merge(self, request, queryset): - if request.POST.get('post'): # Has the user confirmed which is the master record? + if request.POST.get('post'): # Has the user confirmed which is the master record? try: masterObjectPk = request.POST.get('master') - masterObject = queryset.get(pk = masterObjectPk) + masterObject = queryset.get(pk=masterObjectPk) except ObjectDoesNotExist: - self.message_user(request, "An error occured. Did you select a 'master' record?",level=messages.ERROR) + self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR) return with transaction.atomic(), reversion.create_revision(): - for obj in queryset.exclude(pk = masterObjectPk): + for obj in queryset.exclude(pk=masterObjectPk): events = obj.event_set.all() for event in events: masterObject.event_set.add(event) - obj.delete() + obj.delete() reversion.set_comment('Merging Objects') self.message_user(request, "Objects successfully merged.") return - else: # Present the confirmation screen + else: # Present the confirmation screen class TempForm(ModelForm): class Meta: @@ -90,19 +93,23 @@ class AssociateAdmin(reversion.VersionAdmin): 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 'forms': forms } - return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context, current_app=self.admin_site.name) + return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context, + current_app=self.admin_site.name) + @admin.register(models.Person) class PersonAdmin(AssociateAdmin): - list_display = ('id', 'name','phone','email','number_of_events') - merge_fields = ['name','phone','email','address','notes'] + list_display = ('id', 'name', 'phone', 'email', 'number_of_events') + merge_fields = ['name', 'phone', 'email', 'address', 'notes'] + @admin.register(models.Venue) class VenueAdmin(AssociateAdmin): - list_display = ('id', 'name','phone','email','number_of_events') - merge_fields = ['name','phone','email','address','notes','three_phase_available'] + list_display = ('id', 'name', 'phone', 'email', 'number_of_events') + merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available'] + @admin.register(models.Organisation) class OrganisationAdmin(AssociateAdmin): - list_display = ('id', 'name','phone','email','number_of_events') - merge_fields = ['name','phone','email','address','notes','union_account'] \ No newline at end of file + list_display = ('id', 'name', 'phone', 'email', 'number_of_events') + merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account'] diff --git a/RIGS/versioning.py b/RIGS/versioning.py index c537de87..36786a74 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -14,7 +14,7 @@ from django.core.exceptions import ObjectDoesNotExist import reversion import simplejson from reversion.models import Version -from django.contrib.contenttypes.models import ContentType # Used to lookup the content_type +from django.contrib.contenttypes.models import ContentType # Used to lookup the content_type from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.db.models import ForeignKey, IntegerField, EmailField, TextField from diff_match_patch import diff_match_patch @@ -29,11 +29,10 @@ logger = logging.getLogger('tec.pyrigs') def model_compare(oldObj, newObj, excluded_keys=[]): # recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects try: - theFields = oldObj._meta.fields #This becomes deprecated in Django 1.8!!!!!!!!!!!!! (but an alternative becomes available) + theFields = oldObj._meta.fields # This becomes deprecated in Django 1.8!!!!!!!!!!!!! (but an alternative becomes available) except AttributeError: theFields = newObj._meta.fields - class FieldCompare(object): def __init__(self, field=None, old=None, new=None): self.field = field @@ -51,13 +50,13 @@ def model_compare(oldObj, newObj, excluded_keys=[]): @property def new(self): - return self.display_value(self._new) + return self.display_value(self._new) @property def long(self): if isinstance(self.field, EmailField): return True - return False + return False @property def linebreaks(self): @@ -76,21 +75,21 @@ def model_compare(oldObj, newObj, excluded_keys=[]): outputDiffs = [] for (op, data) in diffs: - if op == dmp.DIFF_INSERT: - outputDiffs.append({'type':'insert', 'text':data}) - elif op == dmp.DIFF_DELETE: - outputDiffs.append({'type':'delete', 'text':data}) - elif op == dmp.DIFF_EQUAL: - outputDiffs.append({'type':'equal', 'text':data}) + if op == dmp.DIFF_INSERT: + outputDiffs.append({'type': 'insert', 'text': data}) + elif op == dmp.DIFF_DELETE: + outputDiffs.append({'type': 'delete', 'text': data}) + elif op == dmp.DIFF_EQUAL: + outputDiffs.append({'type': 'equal', 'text': data}) return outputDiffs changes = [] for thisField in theFields: name = thisField.name - + if name in excluded_keys: - continue # if we're excluding this field, skip over it + continue # if we're excluding this field, skip over it try: oldValue = getattr(oldObj, name, None) @@ -101,17 +100,18 @@ def model_compare(oldObj, newObj, excluded_keys=[]): newValue = getattr(newObj, name, None) except ObjectDoesNotExist: newValue = None - + try: bothBlank = (not oldValue) and (not newValue) if oldValue != newValue and not bothBlank: - compare = FieldCompare(thisField,oldValue,newValue) + compare = FieldCompare(thisField, oldValue, newValue) changes.append(compare) - except TypeError: # logs issues with naive vs tz-aware datetimes + except TypeError: # logs issues with naive vs tz-aware datetimes logger.error('TypeError when comparing models') - + return changes + def compare_event_items(old, new): # Recieves two event version objects and compares their items, returns an array of ItemCompare objects @@ -126,39 +126,41 @@ def compare_event_items(old, new): self.changes = changes # Build some dicts of what we have - item_dict = {} # build a list of items, key is the item_pk - for version in old_item_versions: # put all the old versions in a list + item_dict = {} # build a list of items, key is the item_pk + for version in old_item_versions: # put all the old versions in a list compare = ItemCompare(old=version.object_version.object) item_dict[version.object_id] = compare - for version in new_item_versions: # go through the new versions - try: - compare = item_dict[version.object_id] # see if there's a matching old version - compare.new = version.object_version.object # then add the new version to the dictionary - except KeyError: # there's no matching old version, so add this item to the dictionary by itself + for version in new_item_versions: # go through the new versions + try: + compare = item_dict[version.object_id] # see if there's a matching old version + compare.new = version.object_version.object # then add the new version to the dictionary + except KeyError: # there's no matching old version, so add this item to the dictionary by itself compare = ItemCompare(new=version.object_version.object) - - item_dict[version.object_id] = compare # update the dictionary with the changes - changes = [] + item_dict[version.object_id] = compare # update the dictionary with the changes + + changes = [] for (_, compare) in item_dict.items(): - compare.changes = model_compare(compare.old, compare.new, ['id','event','order']) # see what's changed + compare.changes = model_compare(compare.old, compare.new, ['id', 'event', 'order']) # see what's changed if len(compare.changes) >= 1: - changes.append(compare) # transfer into a sequential array to make it easier to deal with later + changes.append(compare) # transfer into a sequential array to make it easier to deal with later return changes + def get_versions_for_model(models): content_types = [] for model in models: content_types.append(ContentType.objects.get_for_model(model)) - + versions = reversion.models.Version.objects.filter( - content_type__in = content_types, + content_type__in=content_types, ).select_related("revision").order_by("-pk") return versions + def get_previous_version(version): thisId = version.object_id thisVersionId = version.pk @@ -166,17 +168,19 @@ def get_previous_version(version): versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId) try: - previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(field_name='revision__date_created') + previousVersions = versions.filter(revision_id__lt=version.revision_id).latest( + field_name='revision__date_created') except ObjectDoesNotExist: return False return previousVersions -def get_changes_for_version(newVersion, oldVersion=None): - #Pass in a previous version if you already know it (for efficiancy) - #if not provided then it will be looked up in the database - if oldVersion == None: +def get_changes_for_version(newVersion, oldVersion=None): + # Pass in a previous version if you already know it (for efficiancy) + # if not provided then it will be looked up in the database + + if oldVersion == None: oldVersion = get_previous_version(newVersion) modelClass = newVersion.content_type.model_class() @@ -200,6 +204,7 @@ def get_changes_for_version(newVersion, oldVersion=None): return compare + class VersionHistory(generic.ListView): model = reversion.revisions.Version template_name = "RIGS/version_history.html" @@ -215,7 +220,7 @@ class VersionHistory(generic.ListView): def get_context_data(self, **kwargs): thisModel = self.kwargs['model'] - + context = super(VersionHistory, self).get_context_data(**kwargs) versions = context['object_list'] @@ -224,81 +229,82 @@ class VersionHistory(generic.ListView): items = [] for versionNo, thisVersion in enumerate(versions): - if versionNo >= len(versions)-1: + if versionNo >= len(versions) - 1: thisItem = get_changes_for_version(thisVersion, None) else: - thisItem = get_changes_for_version(thisVersion, versions[versionNo+1]) - + thisItem = get_changes_for_version(thisVersion, versions[versionNo + 1]) + items.append(thisItem) context['object_list'] = items context['object'] = thisObject - + return context + class ActivityTable(generic.ListView): model = reversion.revisions.Version template_name = "RIGS/activity_table.html" paginate_by = 25 - + def get_queryset(self): - versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation]) + versions = get_versions_for_model([models.Event, models.Venue, models.Person, models.Organisation]) return versions def get_context_data(self, **kwargs): - # Call the base implementation first to get a context context = super(ActivityTable, self).get_context_data(**kwargs) - + items = [] for thisVersion in context['object_list']: thisItem = get_changes_for_version(thisVersion, None) items.append(thisItem) - context ['object_list'] = items - + context['object_list'] = items + return context + class ActivityFeed(generic.ListView): model = reversion.revisions.Version template_name = "RIGS/activity_feed_data.html" paginate_by = 25 - + def get_queryset(self): - versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation]) + versions = get_versions_for_model([models.Event, models.Venue, models.Person, models.Organisation]) return versions def get_context_data(self, **kwargs): maxTimeDelta = [] - maxTimeDelta.append({ 'maxAge':datetime.timedelta(days=1), 'group':datetime.timedelta(hours=1)}) - maxTimeDelta.append({ 'maxAge':None, 'group':datetime.timedelta(days=1)}) + maxTimeDelta.append({'maxAge': datetime.timedelta(days=1), 'group': datetime.timedelta(hours=1)}) + maxTimeDelta.append({'maxAge': None, 'group': datetime.timedelta(days=1)}) # Call the base implementation first to get a context context = super(ActivityFeed, self).get_context_data(**kwargs) - + items = [] for thisVersion in context['object_list']: thisItem = get_changes_for_version(thisVersion, None) if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None: thisItem['withPrevious'] = False - if len(items)>=1: - timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem['revision'].date_created + if len(items) >= 1: + timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem[ + 'revision'].date_created timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created timeTogether = False for params in maxTimeDelta: if params['maxAge'] is None or timeAgo <= params['maxAge']: timeTogether = timeDiff < params['group'] break - + sameUser = thisItem['revision'].user == items[-1]['revision'].user thisItem['withPrevious'] = timeTogether & sameUser items.append(thisItem) - context ['object_list'] = items - + context['object_list'] = items - return context \ No newline at end of file + return context From df61225b73f31dce3f1b17ea35a7cf6f1b9e32ea Mon Sep 17 00:00:00 2001 From: Tom Price Date: Wed, 6 Apr 2016 21:57:25 +0100 Subject: [PATCH 09/67] Optimise imports So many unused imports, these have been banished --- RIGS/versioning.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/RIGS/versioning.py b/RIGS/versioning.py index 36786a74..7677fa13 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -1,27 +1,18 @@ import logging -from django.views import generic -from django.core.urlresolvers import reverse_lazy -from django.shortcuts import get_object_or_404 -from django.template import RequestContext -from django.template.loader import get_template -from django.conf import settings -from django.http import HttpResponse -from django.db.models import Q -from django.contrib import messages + from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import get_object_or_404 +from django.views import generic # Versioning import reversion -import simplejson from reversion.models import Version from django.contrib.contenttypes.models import ContentType # Used to lookup the content_type -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.db.models import ForeignKey, IntegerField, EmailField, TextField +from django.db.models import IntegerField, EmailField, TextField from diff_match_patch import diff_match_patch -from RIGS import models, forms +from RIGS import models import datetime -import re logger = logging.getLogger('tec.pyrigs') From b47cfed5a9886793e6ff53e1f918f25a8f1b7836 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Apr 2016 00:19:18 +0100 Subject: [PATCH 10/67] Fixed versioning UI for revisions containing multiple event versions, hopefully --- RIGS/versioning.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/RIGS/versioning.py b/RIGS/versioning.py index 7677fa13..718bcf95 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -119,17 +119,19 @@ def compare_event_items(old, new): # Build some dicts of what we have item_dict = {} # build a list of items, key is the item_pk for version in old_item_versions: # put all the old versions in a list - compare = ItemCompare(old=version.object_version.object) - item_dict[version.object_id] = compare + if version.object_version.object.event.id == old.object_id: + compare = ItemCompare(old=version.object_version.object) + item_dict[version.object_id] = compare for version in new_item_versions: # go through the new versions - try: - compare = item_dict[version.object_id] # see if there's a matching old version - compare.new = version.object_version.object # then add the new version to the dictionary - except KeyError: # there's no matching old version, so add this item to the dictionary by itself - compare = ItemCompare(new=version.object_version.object) + if version.object_version.object.event.id == new.object_id: + try: + compare = item_dict[version.object_id] # see if there's a matching old version + compare.new = version.object_version.object # then add the new version to the dictionary + except KeyError: # there's no matching old version, so add this item to the dictionary by itself + compare = ItemCompare(new=version.object_version.object) - item_dict[version.object_id] = compare # update the dictionary with the changes + item_dict[version.object_id] = compare # update the dictionary with the changes changes = [] for (_, compare) in item_dict.items(): From f1bd1ca674b9e554402b6c08bc71157f624b5f1b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Apr 2016 00:52:33 +0100 Subject: [PATCH 11/67] Make 'created' noun class-specific. Closes #185 --- RIGS/templates/RIGS/activity_table.html | 2 +- RIGS/templates/RIGS/version_history.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/templates/RIGS/activity_table.html b/RIGS/templates/RIGS/activity_table.html index 1d491663..049e9aad 100644 --- a/RIGS/templates/RIGS/activity_table.html +++ b/RIGS/templates/RIGS/activity_table.html @@ -72,7 +72,7 @@ {{ version.revision.user.name }} {% if version.old == None %} - Object Created + {{object|to_class_name}} Created {% else %} {% include 'RIGS/version_changes.html' %} {% endif %} diff --git a/RIGS/templates/RIGS/version_history.html b/RIGS/templates/RIGS/version_history.html index 924ea148..3261c5df 100644 --- a/RIGS/templates/RIGS/version_history.html +++ b/RIGS/templates/RIGS/version_history.html @@ -47,7 +47,7 @@ {{ version.revision.user.name }} {% if version.old == None %} - Object Created + {{object|to_class_name}} Created {% else %} {% include 'RIGS/version_changes.html' %} {% endif %} From 767b5512e312d7b152c3a8a866146d47c07a07ed Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Apr 2016 00:53:57 +0100 Subject: [PATCH 12/67] Fail, missed a change, actually closes #185 now --- RIGS/templates/RIGS/activity_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/templates/RIGS/activity_table.html b/RIGS/templates/RIGS/activity_table.html index 049e9aad..1a1c8c1d 100644 --- a/RIGS/templates/RIGS/activity_table.html +++ b/RIGS/templates/RIGS/activity_table.html @@ -72,7 +72,7 @@ {{ version.revision.user.name }} {% if version.old == None %} - {{object|to_class_name}} Created + {{version.new|to_class_name}} Created {% else %} {% include 'RIGS/version_changes.html' %} {% endif %} From f8a2a7a9593bc63a8922fa9200d096f9b3ca5325 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Apr 2016 09:23:35 +0100 Subject: [PATCH 13/67] Made items display in version history again --- RIGS/versioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/versioning.py b/RIGS/versioning.py index 718bcf95..ee3da1d8 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -119,12 +119,12 @@ def compare_event_items(old, new): # Build some dicts of what we have item_dict = {} # build a list of items, key is the item_pk for version in old_item_versions: # put all the old versions in a list - if version.object_version.object.event.id == old.object_id: + if version.object_version.object.event.id == old.object_id_int: compare = ItemCompare(old=version.object_version.object) item_dict[version.object_id] = compare for version in new_item_versions: # go through the new versions - if version.object_version.object.event.id == new.object_id: + if version.object_version.object.event.id == new.object_id_int: try: compare = item_dict[version.object_id] # see if there's a matching old version compare.new = version.object_version.object # then add the new version to the dictionary From 98f28aaafd0a5756f5346abacbaecc45529ade36 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 12 Apr 2016 20:15:13 +0100 Subject: [PATCH 14/67] Based on has name as well as MIC - Fix #224 --- RIGS/templates/RIGS/event_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/templates/RIGS/event_detail.html b/RIGS/templates/RIGS/event_detail.html index e68e73bf..81658663 100644 --- a/RIGS/templates/RIGS/event_detail.html +++ b/RIGS/templates/RIGS/event_detail.html @@ -151,7 +151,7 @@ {% if object.based_on.is_rig %}N{{ object.based_on.pk|stringformat:"05d" }}{% else %} {{ object.based_on.pk }}{% endif %} - {{ object.base_on.name }} {% if object.based_on.mic %}by {{ object.based_on.mic.name }}{% endif %} + {{ object.based_on.name }} {% if object.based_on.mic %}by {{ object.based_on.mic.name }}{% endif %} {% endif %} From 29aa13316df95eaab7ab1af9d85e0a73dcd774b5 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 27 Apr 2016 20:49:42 +0100 Subject: [PATCH 15/67] Fix for versioning when event has been deleted, closes #226 --- RIGS/versioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/versioning.py b/RIGS/versioning.py index ee3da1d8..6d524fc0 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -119,12 +119,12 @@ def compare_event_items(old, new): # Build some dicts of what we have item_dict = {} # build a list of items, key is the item_pk for version in old_item_versions: # put all the old versions in a list - if version.object_version.object.event.id == old.object_id_int: + if version.field_dict["event"] == old.object_id_int: compare = ItemCompare(old=version.object_version.object) item_dict[version.object_id] = compare for version in new_item_versions: # go through the new versions - if version.object_version.object.event.id == new.object_id_int: + if version.field_dict["event"] == new.object_id_int: try: compare = item_dict[version.object_id] # see if there's a matching old version compare.new = version.object_version.object # then add the new version to the dictionary From ef815360663034e03948dc75ae4c5d43780fd1be Mon Sep 17 00:00:00 2001 From: Tom Price Date: Thu, 19 May 2016 14:21:21 +0100 Subject: [PATCH 16/67] Add tooltips to rig type selectors. Closes #227 --- RIGS/templates/RIGS/event_form.html | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/RIGS/templates/RIGS/event_form.html b/RIGS/templates/RIGS/event_form.html index 0e7aa6ad..0b855f17 100644 --- a/RIGS/templates/RIGS/event_form.html +++ b/RIGS/templates/RIGS/event_form.html @@ -29,9 +29,9 @@ } function setTime02Hours() { - var id_start = "{{ form.start_date.id_for_label }}" - var id_end_date = "{{ form.end_date.id_for_label }}" - var id_end_time = "{{ form.end_time.id_for_label }}" + var id_start = "{{ form.start_date.id_for_label }}"; + var id_end_date = "{{ form.end_date.id_for_label }}"; + var id_end_time = "{{ form.end_time.id_for_label }}"; if ($('#'+id_start).val() == $('#'+id_end_date).val()) { var end_date = new Date($('#'+id_end_date).val()); end_date.setDate(end_date.getDate() + 1); @@ -67,7 +67,7 @@ $('#{{form.is_rig.auto_id}}').prop('checked', false); $('.form-is_rig').slideUp(); } - }) + }); {% endif %} function supportsDate() { @@ -106,7 +106,7 @@ }); } - }) + }); $(document).ready(function () { setupItemTable($("#{{ form.items_json.id_for_label }}").val()); @@ -150,10 +150,12 @@
- + - +
From 2913b254b4fb808a281a9952abfb304c000200d9 Mon Sep 17 00:00:00 2001 From: Tom Price Date: Thu, 19 May 2016 14:33:33 +0100 Subject: [PATCH 17/67] Fix an overflow issue with dropdown fields. Closes #204 --- RIGS/templates/RIGS/event_form.html | 1 + 1 file changed, 1 insertion(+) diff --git a/RIGS/templates/RIGS/event_form.html b/RIGS/templates/RIGS/event_form.html index 0b855f17..28e307ca 100644 --- a/RIGS/templates/RIGS/event_form.html +++ b/RIGS/templates/RIGS/event_form.html @@ -63,6 +63,7 @@ } else { $('.form-is_rig').slideDown(); } + $('.form-hws').css('overflow', 'visible'); } else { $('#{{form.is_rig.auto_id}}').prop('checked', false); $('.form-is_rig').slideUp(); From 1faf8f95c8f8b9b6a678e43af4982c5e75209806 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 22 May 2016 23:13:23 +0100 Subject: [PATCH 18/67] Fixed colour coding on invoice waiting page --- RIGS/templates/RIGS/event_invoice.html | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index c16c501b..805eb3d9 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -26,16 +26,18 @@ {% for object in object_list %} N{{ object.pk|stringformat:"05d" }} {{ object.end_date }} From e4a955f3239cf285f8e7c62bef57f21d7fa531fd Mon Sep 17 00:00:00 2001 From: Tom Price Date: Mon, 23 May 2016 12:36:21 +0100 Subject: [PATCH 19/67] Reformat code to PEP8 --- RIGS/finance.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/RIGS/finance.py b/RIGS/finance.py index 73552f2e..d719c66f 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -1,20 +1,19 @@ import cStringIO as StringIO +import datetime +import re +from django.contrib import messages from django.core.urlresolvers import reverse_lazy -from django.db import connection from django.http import Http404, HttpResponseRedirect -from django.views import generic -from django.template import RequestContext -from django.template.loader import get_template from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from django.contrib import messages -import datetime +from django.template import RequestContext +from django.template.loader import get_template +from django.views import generic from z3c.rml import rml2pdf from RIGS import models -import re class InvoiceIndex(generic.ListView): model = models.Invoice @@ -40,6 +39,7 @@ class InvoiceIndex(generic.ListView): class InvoiceDetail(generic.DetailView): model = models.Invoice + class InvoicePrint(generic.View): def get(self, request, pk): invoice = get_object_or_404(models.Invoice, pk=pk) @@ -54,8 +54,8 @@ class InvoicePrint(generic.View): 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF', } }, - 'invoice':invoice, - 'current_user':request.user, + 'invoice': invoice, + 'current_user': request.user, }) rml = template.render(context) @@ -72,6 +72,7 @@ class InvoicePrint(generic.View): response.write(pdfData) return response + class InvoiceVoid(generic.View): def get(self, *args, **kwargs): pk = kwargs.get('pk') @@ -119,7 +120,7 @@ class InvoiceEvent(generic.View): class PaymentCreate(generic.CreateView): model = models.Payment - fields = ['invoice','date','amount','method'] + fields = ['invoice', 'date', 'amount', 'method'] def get_initial(self): initial = super(generic.CreateView, self).get_initial() @@ -139,4 +140,4 @@ class PaymentDelete(generic.DeleteView): model = models.Payment def get_success_url(self): - return self.request.POST.get('next') \ No newline at end of file + return self.request.POST.get('next') From 60302889569679ad9d41be940b2b50953f4b1761 Mon Sep 17 00:00:00 2001 From: Tom Price Date: Tue, 24 May 2016 17:17:52 +0100 Subject: [PATCH 20/67] Cheap and dirty active totals --- RIGS/finance.py | 14 ++- RIGS/models.py | 125 +++++++++++++------------- RIGS/templates/RIGS/invoice_list.html | 2 +- 3 files changed, 77 insertions(+), 64 deletions(-) diff --git a/RIGS/finance.py b/RIGS/finance.py index d719c66f..57721a9f 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -19,16 +19,24 @@ class InvoiceIndex(generic.ListView): model = models.Invoice template_name = 'RIGS/invoice_list.html' + def get_context_data(self, **kwargs): + context = super(InvoiceIndex, self).get_context_data(**kwargs) + total = 0 + for i in context['object_list']: + total += i.balance + context['total'] = total + return context + def get_queryset(self): # Manual query is the only way I have found to do this efficiently. Not ideal but needs must sql = "SELECT * FROM " \ "(SELECT " \ - "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \ + "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \ "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \ - "(SELECT SUM(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ + "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \ "AS sub " \ - "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \ + "WHERE (cost - payments) <> 0.0 AND void = '0'" \ "ORDER BY invoice_date" query = self.model.objects.raw(sql) diff --git a/RIGS/models.py b/RIGS/models.py index fc06911f..535aa42c 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -1,31 +1,32 @@ +import datetime import hashlib -import datetime, pytz - -from django.db import models, connection -from django.contrib.auth.models import AbstractUser -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 pytz import random +import string from collections import Counter -from django.core.urlresolvers import reverse_lazy -from django.core.exceptions import ValidationError - from decimal import Decimal +import reversion +from django.conf import settings +from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse_lazy +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property + + # Create your models here. @python_2_unicode_compatible 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) + 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 + size = 20 + chars = string.ascii_letters + string.digits new_api_key = ''.join(random.choice(chars) for x in range(size)) return new_api_key; @@ -55,6 +56,7 @@ class Profile(AbstractUser): ('view_profile', 'Can view Profile'), ) + class RevisionMixin(object): @property def last_edited_at(self): @@ -79,10 +81,11 @@ class RevisionMixin(object): versions = reversion.get_for_object(self) if versions: version = reversion.get_for_object(self)[0] - return "V{0} | R{1}".format(version.pk,version.revision.pk) + return "V{0} | R{1}".format(version.pk, version.revision.pk) else: return None + @reversion.register @python_2_unicode_compatible class Person(models.Model, RevisionMixin): @@ -97,7 +100,7 @@ class Person(models.Model, RevisionMixin): def __str__(self): string = self.name if self.notes is not None: - if len(self.notes) > 0: + if len(self.notes) > 0: string += "*" return string @@ -108,7 +111,7 @@ class Person(models.Model, RevisionMixin): if e.organisation: o.append(e.organisation) - #Count up occurances and put them in descending order + # Count up occurances and put them in descending order c = Counter(o) stats = c.most_common() return stats @@ -141,7 +144,7 @@ class Organisation(models.Model, RevisionMixin): def __str__(self): string = self.name if self.notes is not None: - if len(self.notes) > 0: + if len(self.notes) > 0: string += "*" return string @@ -151,8 +154,8 @@ class Organisation(models.Model, RevisionMixin): for e in Event.objects.filter(organisation=self).select_related('person'): if e.person: p.append(e.person) - - #Count up occurances and put them in descending order + + # Count up occurances and put them in descending order c = Counter(p) stats = c.most_common() return stats @@ -238,12 +241,18 @@ class Venue(models.Model, RevisionMixin): class EventManager(models.Manager): def current_events(self): events = self.filter( - (models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end - (models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after - (models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire - (models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT + (models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q( + status=Event.CANCELLED)) | # Starts after with no end + (models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q( + status=Event.CANCELLED)) | # Ends after + (models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q( + status=Event.CANCELLED)) | # Active dry hire + (models.Q(dry_hire=True, checked_in_by__isnull=True) & ( + models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') + ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', + 'organisation', + 'venue', 'mic') return events def events_in_bounds(self, start, end): @@ -251,15 +260,17 @@ class EventManager(models.Manager): (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds (models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds - (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds + (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds - (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after - (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after - (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after - (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after - (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after + (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after + (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after + (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after + (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after + (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') + ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', + 'organisation', + 'venue', 'mic') return events def rig_count(self): @@ -301,7 +312,8 @@ class Event(models.Model, RevisionMixin): status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) dry_hire = models.BooleanField(default=False) is_rig = models.BooleanField(default=True) - based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, null=True) + based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, + null=True) # Timing start_date = models.DateField() @@ -327,6 +339,7 @@ class Event(models.Model, RevisionMixin): """ EX Vat """ + @property def sum_total(self): # Manual querying is required for efficiency whilst maintaining floating point arithmetic @@ -334,14 +347,15 @@ class Event(models.Model, RevisionMixin): # sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id # else: # sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id - #total = self.items.raw(sql)[0] - #if total.sum_total: + # total = self.items.raw(sql)[0] + # if total.sum_total: # return total.sum_total - #total = 0.0 - #for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"): + # total = 0.0 + # for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"): # total += item.sum total = EventItem.objects.filter(event=self).aggregate( - sum_total=models.Sum(models.F('cost')*models.F('quantity'), output_field=models.DecimalField(max_digits=10, decimal_places=2)) + sum_total=models.Sum(models.F('cost') * models.F('quantity'), + output_field=models.DecimalField(max_digits=10, decimal_places=2)) )['sum_total'] if total: return total @@ -358,6 +372,7 @@ class Event(models.Model, RevisionMixin): """ Inc VAT """ + @property def total(self): return self.sum_total + self.vat @@ -382,7 +397,7 @@ class Event(models.Model, RevisionMixin): def earliest_time(self): """Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object""" - #Put all the datetimes in a list + # Put all the datetimes in a list datetime_list = [] if self.access_at: @@ -394,22 +409,22 @@ class Event(models.Model, RevisionMixin): # If there is no start time defined, pretend it's midnight startTimeFaked = False if self.has_start_time: - startDateTime = datetime.datetime.combine(self.start_date,self.start_time) + startDateTime = datetime.datetime.combine(self.start_date, self.start_time) else: - startDateTime = datetime.datetime.combine(self.start_date,datetime.time(00,00)) + startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00)) startTimeFaked = True - #timezoneIssues - apply the default timezone to the naiive datetime + # timezoneIssues - apply the default timezone to the naiive datetime tz = pytz.timezone(settings.TIME_ZONE) startDateTime = tz.localize(startDateTime) - datetime_list.append(startDateTime) # then add it to the list + datetime_list.append(startDateTime) # then add it to the list - earliest = min(datetime_list).astimezone(tz) #find the earliest datetime in the list + earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list # if we faked it & it's the earliest, better own up - if startTimeFaked and earliest==startDateTime: + if startTimeFaked and earliest == startDateTime: return self.start_date - + return earliest @property @@ -421,7 +436,7 @@ class Event(models.Model, RevisionMixin): endDate = self.start_date if self.has_end_time: - endDateTime = datetime.datetime.combine(endDate,self.end_time) + endDateTime = datetime.datetime.combine(endDate, self.end_time) tz = pytz.timezone(settings.TIME_ZONE) endDateTime = tz.localize(endDateTime) @@ -430,7 +445,6 @@ class Event(models.Model, RevisionMixin): else: return endDate - objects = EventManager() def get_absolute_url(self): @@ -446,7 +460,7 @@ class Event(models.Model, RevisionMixin): startEndSameDay = not self.end_date or self.end_date == self.start_date hasStartAndEnd = self.has_start_time and self.has_end_time if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time: - raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') + raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') def save(self, *args, **kwargs): """Call :meth:`full_clean` before saving.""" @@ -503,15 +517,6 @@ class Invoice(models.Model): @property def payment_total(self): - # Manual querying is required for efficiency whilst maintaining floating point arithmetic - #if connection.vendor == 'postgresql': - # sql = "SELECT SUM(amount) AS total FROM \"RIGS_payment\" WHERE invoice_id=%i" % self.id - #else: - # sql = "SELECT id, SUM(amount) AS total FROM RIGS_payment WHERE invoice_id=%i" % self.id - #total = self.payment_set.raw(sql)[0] - #if total.total: - # return total.total - #return 0.0 total = self.payment_set.aggregate(total=models.Sum('amount'))['total'] if total: return total @@ -552,4 +557,4 @@ class Payment(models.Model): method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True) def __str__(self): - return "%s: %d" % (self.get_method_display(), self.amount) \ No newline at end of file + return "%s: %d" % (self.get_method_display(), self.amount) diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index bf9fe31f..018ca910 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -5,7 +5,7 @@ {% block content %}
-

Invoices

+

Invoices (£ {{ total|floatformat:2 }})

{% if is_paginated %}
{% paginator %} From 7ccc8faf20a308cfae943aad60a8b77918b459db Mon Sep 17 00:00:00 2001 From: Tom Price Date: Tue, 24 May 2016 17:32:58 +0100 Subject: [PATCH 21/67] Add total for waiting events --- RIGS/finance.py | 8 ++++++++ RIGS/templates/RIGS/event_invoice.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/RIGS/finance.py b/RIGS/finance.py index 57721a9f..9025e15b 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -103,6 +103,14 @@ class InvoiceWaiting(generic.ListView): paginate_by = 25 template_name = 'RIGS/event_invoice.html' + def get_context_data(self, **kwargs): + context = super(InvoiceWaiting, self).get_context_data(**kwargs) + total = 0 + for obj in context['object_list']: + total += obj.sum_total + context['total'] = total + return context + def get_queryset(self): # @todo find a way to select items events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(), diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 805eb3d9..29c56f42 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -5,7 +5,7 @@ {% block content %}
-

Events for Invoice

+

Events for Invoice (£ {{ total|floatformat:2 }})

{% if is_paginated %}
{% paginator %} From 6b77393414682c930b3d091361ec76ea7f3fa0cf Mon Sep 17 00:00:00 2001 From: Tom Price Date: Tue, 24 May 2016 17:49:44 +0100 Subject: [PATCH 22/67] Fix for incorrect selection of active invoices. Make sure waiting shows total across all pages. --- RIGS/finance.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/RIGS/finance.py b/RIGS/finance.py index 9025e15b..69eaa513 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -36,7 +36,7 @@ class InvoiceIndex(generic.ListView): "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \ "AS sub " \ - "WHERE (cost - payments) <> 0.0 AND void = '0'" \ + "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \ "ORDER BY invoice_date" query = self.model.objects.raw(sql) @@ -106,19 +106,24 @@ class InvoiceWaiting(generic.ListView): def get_context_data(self, **kwargs): context = super(InvoiceWaiting, self).get_context_data(**kwargs) total = 0 - for obj in context['object_list']: + for obj in self.get_objects(): total += obj.sum_total context['total'] = total return context def get_queryset(self): + return self.get_objects() + + def get_objects(self): # @todo find a way to select items events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(), invoice__isnull=True) \ .order_by('start_date') \ .select_related('person', 'organisation', - 'venue', 'mic') + 'venue', 'mic') \ + .prefetch_related('items') + return events From cc2450ff871467a45920a05051268ebe374e2662 Mon Sep 17 00:00:00 2001 From: Tom Price Date: Tue, 24 May 2016 17:50:48 +0100 Subject: [PATCH 23/67] Make total conditional if defined --- RIGS/templates/RIGS/invoice_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index 018ca910..32abdcec 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -5,7 +5,7 @@ {% block content %}
-

Invoices (£ {{ total|floatformat:2 }})

+

Invoices {% if total %}(£ {{ total|floatformat:2 }}){% endif %}

{% if is_paginated %}
{% paginator %} From db58c113aafe68c131e496d3b8a8cbec490c09e2 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 28 May 2016 14:52:48 +0100 Subject: [PATCH 24/67] Changed font to load over https - #236 --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index c27231bd..3cde5acd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,7 +14,7 @@ - From 000351d884b441485ab0ff65ca31c8c9bd0a7ee1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 28 May 2016 15:20:15 +0100 Subject: [PATCH 25/67] Redirect all requests to https --- PyRIGS/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index d85ff887..bb9ba676 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -13,6 +13,7 @@ import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_SSL_REDIRECT = True # Redirect all http requests to https # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ @@ -55,6 +56,7 @@ INSTALLED_APPS = ( MIDDLEWARE_CLASSES = ( 'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware', + 'django.middleware.security.SecurityMiddleware', 'reversion.middleware.RevisionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', From aa79f3628e157854764eb26248b026a9c3c0898a Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 28 May 2016 15:27:38 +0100 Subject: [PATCH 26/67] Only redirect to HTTPS in production --- PyRIGS/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index bb9ba676..3098c3fb 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -12,9 +12,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -SECURE_SSL_REDIRECT = True # Redirect all http requests to https - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ @@ -28,6 +25,10 @@ TEMPLATE_DEBUG = True ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +if not DEBUG: + SECURE_SSL_REDIRECT = True # Redirect all http requests to https + INTERNAL_IPS = ['127.0.0.1'] ADMINS = ( From a725ef5caf4fb6e1e3beaa4302a2a12e461d6608 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 17:09:52 +0100 Subject: [PATCH 27/67] Removed add to google calendar link, closes #237 --- RIGS/templates/RIGS/profile_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RIGS/templates/RIGS/profile_detail.html b/RIGS/templates/RIGS/profile_detail.html index 8fce1e60..e69e7bf0 100644 --- a/RIGS/templates/RIGS/profile_detail.html +++ b/RIGS/templates/RIGS/profile_detail.html @@ -126,7 +126,7 @@
{% if user.api_key %}

-						Click here to add to google calendar.
+ Click here for instructions on adding to google calendar.
To sync from google calendar to mobile device, visit this page on your device and tick "RIGS Calendar".
{% else %}
No API Key Generated
From eaf5c9687edd82269202f64798ebbc664ba88cac Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 20:21:23 +0100 Subject: [PATCH 28/67] Fixed typo, closes #174 --- PyRIGS/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py index a7c1db90..67d8964c 100644 --- a/PyRIGS/decorators.py +++ b/PyRIGS/decorators.py @@ -58,7 +58,7 @@ def api_key_required(function): try: user_object = models.Profile.objects.get(pk=userid) - except Profile.DoesNotExist: + except models.Profile.DoesNotExist: return error_resp if user_object.api_key != key: From 83302c44399ccf45d1d5431c6ef68520b07ccdc1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 21:30:05 +0100 Subject: [PATCH 29/67] Invoice UI improvements. Renamed pages, added description, and added total number of events --- RIGS/finance.py | 3 ++- RIGS/templates/RIGS/event_invoice.html | 3 ++- RIGS/templates/RIGS/invoice_list.html | 3 ++- RIGS/templates/RIGS/invoice_list_active.html | 13 +++++++++++++ RIGS/templates/RIGS/invoice_list_archive.html | 13 +++++++++++++ templates/base.html | 2 +- 6 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 RIGS/templates/RIGS/invoice_list_active.html create mode 100644 RIGS/templates/RIGS/invoice_list_archive.html diff --git a/RIGS/finance.py b/RIGS/finance.py index 69eaa513..d604cd57 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -17,7 +17,7 @@ from RIGS import models class InvoiceIndex(generic.ListView): model = models.Invoice - template_name = 'RIGS/invoice_list.html' + template_name = 'RIGS/invoice_list_active.html' def get_context_data(self, **kwargs): context = super(InvoiceIndex, self).get_context_data(**kwargs) @@ -95,6 +95,7 @@ class InvoiceVoid(generic.View): class InvoiceArchive(generic.ListView): model = models.Invoice + template_name = 'RIGS/invoice_list_archive.html' paginate_by = 25 diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 29c56f42..3324911a 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -5,7 +5,8 @@ {% block content %}
-

Events for Invoice (£ {{ total|floatformat:2 }})

+

Events for Invoice ({{object_list|length}} Events, £ {{ total|floatformat:2 }})

+

These events have happened, but paperwork has not yet been sent to treasury

{% if is_paginated %}
{% paginator %} diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index 32abdcec..26ddeb12 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -5,7 +5,8 @@ {% block content %}
-

Invoices {% if total %}(£ {{ total|floatformat:2 }}){% endif %}

+

{% block heading %}Invoices{% endblock %}

+ {% block description %}{% endblock %} {% if is_paginated %}
{% paginator %} diff --git a/RIGS/templates/RIGS/invoice_list_active.html b/RIGS/templates/RIGS/invoice_list_active.html new file mode 100644 index 00000000..24cea673 --- /dev/null +++ b/RIGS/templates/RIGS/invoice_list_active.html @@ -0,0 +1,13 @@ +{% extends 'RIGS/invoice_list.html' %} + +{% block title %} +Outstanding Invoices +{% endblock %} + +{% block heading %} +Outstanding Invoices ({{object_list|length}} Events, £ {{ total|floatformat:2 }}) +{% endblock %} + +{% block description %} +

Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger

+{% endblock %} \ No newline at end of file diff --git a/RIGS/templates/RIGS/invoice_list_archive.html b/RIGS/templates/RIGS/invoice_list_archive.html new file mode 100644 index 00000000..77bab204 --- /dev/null +++ b/RIGS/templates/RIGS/invoice_list_archive.html @@ -0,0 +1,13 @@ +{% extends 'RIGS/invoice_list.html' %} + +{% block title %} +Invoice Archive +{% endblock %} + +{% block heading %} +All Invoices +{% endblock %} + +{% block description %} +

This page displays all invoices: outstanding, paid, and void

+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 3cde5acd..299be06e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -74,7 +74,7 @@
+ {% endif %}
From 3ccbdff737cca2c7390bf511ee990cfed9efbbce Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 22:51:41 +0100 Subject: [PATCH 35/67] Added balance to invoice page - closes #235 --- RIGS/templates/RIGS/invoice_detail.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html index eccec0a3..e5b14a7f 100644 --- a/RIGS/templates/RIGS/invoice_detail.html +++ b/RIGS/templates/RIGS/invoice_detail.html @@ -111,6 +111,11 @@ {% endfor %} + + Balance: + {{ object.balance|floatformat:2 }} + +
From a48afb91573d9bb59c03c8323786a462f3982d22 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 22:56:58 +0100 Subject: [PATCH 36/67] Added internal/external indicators to invoice lists --- RIGS/templates/RIGS/event_invoice.html | 5 +++++ RIGS/templates/RIGS/invoice_list.html | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 94d84f04..79098853 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -57,9 +57,14 @@ {% if object.organisation %} {{ object.organisation.name }} +
+ {{ object.organisation.union_account|yesno:'Internal,External' }} {% else %} {{ object.person.name }} +
+ External {% endif %} + {{ object.sum_total|floatformat:2 }} diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index cb63b7f5..f7d23fcf 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -33,9 +33,14 @@ {% if object.organisation %} {{ object.event.organisation.name }} +
+ {{ object.organisation.union_account|yesno:'Internal,External' }} {% else %} {{ object.event.person.name }} - {% endif %} +
+ External + {% endif %} + {{ object.event.start_date }} {{ object.invoice_date }} {{ object.balance|floatformat:2 }} From 5cc69cbb413ac1adbc615f324cfed28f27ea5f31 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 23:03:41 +0100 Subject: [PATCH 37/67] Stopped things opening in a new window, because it's really annoying. If you want to do this, use the appropriate keyboard shortcut or mouse button --- RIGS/templates/RIGS/event_invoice.html | 4 ++-- RIGS/templates/RIGS/invoice_list.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 79098853..0e56158f 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -50,7 +50,7 @@ danger {% endif %} "> - N{{ object.pk|stringformat:"05d" }}
+ N{{ object.pk|stringformat:"05d" }}
{{ object.get_status_display }} {{ object.start_date }} {{ object.name }} @@ -76,7 +76,7 @@ {% endif %} - + diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index f7d23fcf..f833b89a 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -28,7 +28,7 @@ {% for object in object_list %} {{ object.pk }} - N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
+ N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
{{ object.event.get_status_display }} {% if object.organisation %} From 68b35c2d249763f9d5e60a717f9629ebabe6b226 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 29 May 2016 23:47:56 +0100 Subject: [PATCH 38/67] Removed print button conditions following discussion --- RIGS/templates/RIGS/invoice_detail.html | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html index e5b14a7f..ad177ba4 100644 --- a/RIGS/templates/RIGS/invoice_detail.html +++ b/RIGS/templates/RIGS/invoice_detail.html @@ -15,12 +15,10 @@ - {% if not object.event.organisation.union_account %} - -
- {% endif %} + +
From 9108cb3c4e264b38455b99c57b35deb0398912dc Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 4 Jun 2016 18:08:43 +0100 Subject: [PATCH 39/67] Fail! Hide non-rigs from waiting invoices --- RIGS/finance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/RIGS/finance.py b/RIGS/finance.py index be52b1f1..1fd9b535 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -124,6 +124,7 @@ class InvoiceWaiting(generic.ListView): Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end Q(end_date__lte=datetime.date.today()) # Has end date, finishes before ) & Q(invoice__isnull=True) # Has not already been invoiced + & Q(is_rig=True) # Is a rig (not non-rig) ).order_by('start_date') \ .select_related('person', From f92f418bc5cff671930a52bdab319cc8704553c1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 6 Jun 2016 23:06:30 +0100 Subject: [PATCH 40/67] Fixed waiting invoice counter - closes #239 --- RIGS/finance.py | 1 + RIGS/templates/RIGS/event_invoice.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RIGS/finance.py b/RIGS/finance.py index 1fd9b535..75de8bd9 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -112,6 +112,7 @@ class InvoiceWaiting(generic.ListView): for obj in self.get_objects(): total += obj.sum_total context['total'] = total + context['count'] = len(self.get_objects()) return context def get_queryset(self): diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 0e56158f..3136fe40 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -15,7 +15,7 @@ {% block content %}
-

Events for Invoice ({{object_list|length}} Events, £ {{ total|floatformat:2 }})

+

Events for Invoice ({{count}} Events, £ {{ total|floatformat:2 }})

These events have happened, but paperwork has not yet been sent to treasury

{% if is_paginated %}
From 1163b117e4020421b0b4ce33f7abec330c8a5505 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 14 Jun 2016 19:23:40 +0100 Subject: [PATCH 41/67] Fixed #240 --- RIGS/templates/RIGS/invoice_list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index f833b89a..7b080897 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -31,10 +31,10 @@ N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
{{ object.event.get_status_display }} - {% if object.organisation %} + {% if object.event.organisation %} {{ object.event.organisation.name }}
- {{ object.organisation.union_account|yesno:'Internal,External' }} + {{ object.event.organisation.union_account|yesno:'Internal,External' }} {% else %} {{ object.event.person.name }}
From f265da2f1d9277a404c849d1004fb7a2907c6df0 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 14 Jun 2016 19:45:45 +0100 Subject: [PATCH 42/67] Fixed #241 and #244 --- RIGS/templates/RIGS/event_invoice.html | 124 +++++++++++++------------ RIGS/templates/RIGS/invoice_list.html | 82 ++++++++-------- 2 files changed, 105 insertions(+), 101 deletions(-) diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 3136fe40..05b351aa 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -22,68 +22,70 @@ {% paginator %}
{% endif %} - - - - - - - - - - - - - - {% for object in object_list %} - - - - - - - - +
+
#Start DateEvent NameClientCost
{{ object.start_date }}{{ object.name }} - {% if object.organisation %} - {{ object.organisation.name }} -
- {{ object.organisation.union_account|yesno:'Internal,External' }} - {% else %} - {{ object.person.name }} -
- External - {% endif %} - -
{{ object.sum_total|floatformat:2 }} - {% if object.mic %} - {{ object.mic.initials }}
- - {% else %} - - {% endif %} -
- - - -
+ + + + + + + + + - {% endfor %} - -
#Start DateEvent NameClientCostMIC
+ + + {% for object in object_list %} + + N{{ object.pk|stringformat:"05d" }}
+ {{ object.get_status_display }} + {{ object.start_date }} + {{ object.name }} + + {% if object.organisation %} + {{ object.organisation.name }} +
+ {{ object.organisation.union_account|yesno:'Internal,External' }} + {% else %} + {{ object.person.name }} +
+ External + {% endif %} + + + {{ object.sum_total|floatformat:2 }} + + {% if object.mic %} + {{ object.mic.initials }}
+ + {% else %} + + {% endif %} + + + + + + + + {% endfor %} + + +
{% if is_paginated %}
{% paginator %} diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index 7b080897..2c77d352 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -12,47 +12,49 @@ {% paginator %}
{% endif %} - - - - - - - - - - - - - - {% for object in object_list %} - - - - - - - - - +
+
#EventClientEvent DateInvoice DateBalance
{{ object.pk }}N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
- {{ object.event.get_status_display }}
{% if object.event.organisation %} - {{ object.event.organisation.name }} -
- {{ object.event.organisation.union_account|yesno:'Internal,External' }} - {% else %} - {{ object.event.person.name }} -
- External - {% endif %} -
{{ object.event.start_date }}{{ object.invoice_date }}{{ object.balance|floatformat:2 }} - - - -
+ + + + + + + + + - {% endfor %} - -
#EventClientEvent DateInvoice DateBalance
+ + + {% for object in object_list %} + + {{ object.pk }} + N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
+ {{ object.event.get_status_display }} + + {% if object.event.organisation %} + {{ object.event.organisation.name }} +
+ {{ object.event.organisation.union_account|yesno:'Internal,External' }} + {% else %} + {{ object.event.person.name }} +
+ External + {% endif %} + + {{ object.event.start_date }} + {{ object.invoice_date }} + {{ object.balance|floatformat:2 }} + + + + + + + {% endfor %} + + +
{% if is_paginated %}
{% paginator %} From a7247c273edaf26e73a520cc753807f671f8177f Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 14 Jun 2016 19:50:35 +0100 Subject: [PATCH 43/67] Fixed #245 --- 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 1868f2dd..949c710a 100644 --- a/RIGS/templates/RIGS/event_table.html +++ b/RIGS/templates/RIGS/event_table.html @@ -1,7 +1,7 @@
- + @@ -23,7 +23,7 @@ danger {% endif %} "> - +
# Event Date Event Details Event Timings{{ event.pk }}
{{ event.start_date|date:"D d/m/Y" }}
{% if event.end_date and event.end_date != event.start_date %} From e1578eb0d47b8d5a17a57b23b828e32b61a265cc Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 15 Jun 2016 13:00:14 +0100 Subject: [PATCH 44/67] Fixed responsive table fail --- RIGS/templates/RIGS/event_invoice.html | 2 +- RIGS/templates/RIGS/invoice_list.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 05b351aa..cd080a74 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -22,7 +22,7 @@ {% paginator %} {% endif %} -
+
diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index 2c77d352..5bd7ef70 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -12,7 +12,7 @@ {% paginator %} {% endif %} -
+
From 67624eea6f2bae4f44f5c7ae179182d67aed1949 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 15 Jun 2016 23:18:46 +0100 Subject: [PATCH 45/67] Allow deleting invoices, if there are no payments yet --- RIGS/finance.py | 19 ++++++++++ .../RIGS/invoice_confirm_delete.html | 35 +++++++++++++++++++ RIGS/templates/RIGS/invoice_detail.html | 4 +++ RIGS/urls.py | 3 ++ 4 files changed, 61 insertions(+) create mode 100644 RIGS/templates/RIGS/invoice_confirm_delete.html diff --git a/RIGS/finance.py b/RIGS/finance.py index 75de8bd9..65fc3b07 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -94,6 +94,25 @@ class InvoiceVoid(generic.View): return HttpResponseRedirect(reverse_lazy('invoice_list')) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk})) +class InvoiceDelete(generic.DeleteView): + model = models.Invoice + + def get(self, request, pk): + obj = self.get_object() + if obj.payment_set.all().count() > 0: + messages.info(self.request, 'To delete an invoice, delete the payments first.') + return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk})) + return super(InvoiceDelete, self).get(pk) + + def post(self, request, pk): + obj = self.get_object() + if obj.payment_set.all().count() > 0: + messages.info(self.request, 'To delete an invoice, delete the payments first.') + return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk})) + return super(InvoiceDelete, self).post(pk) + + def get_success_url(self): + return self.request.POST.get('next') class InvoiceArchive(generic.ListView): model = models.Invoice diff --git a/RIGS/templates/RIGS/invoice_confirm_delete.html b/RIGS/templates/RIGS/invoice_confirm_delete.html new file mode 100644 index 00000000..fe295ed6 --- /dev/null +++ b/RIGS/templates/RIGS/invoice_confirm_delete.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} + +{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %} + +{% block content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html index ad177ba4..27b4c514 100644 --- a/RIGS/templates/RIGS/invoice_detail.html +++ b/RIGS/templates/RIGS/invoice_detail.html @@ -11,6 +11,10 @@
+ + + diff --git a/RIGS/urls.py b/RIGS/urls.py index 4338df08..ee949ff8 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -127,6 +127,9 @@ urlpatterns = patterns('', url(r'^invoice/(?P\d+)/void/$', permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()), name='invoice_void'), + url(r'^invoice/(?P\d+)/delete/$', + permission_required_with_403('RIGS.change_invoice')(finance.InvoiceDelete.as_view()), + name='invoice_delete'), url(r'^payment/create/$', permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()), name='payment_create'), From 667b0c80ca7bc7e8fd2eae8839f969d61c46dcd1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 16 Jun 2016 01:44:59 +0100 Subject: [PATCH 46/67] Added tests for invoice deleting --- RIGS/test_unit.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/RIGS/test_unit.py b/RIGS/test_unit.py index 75fcca5a..c51874f4 100644 --- a/RIGS/test_unit.py +++ b/RIGS/test_unit.py @@ -155,3 +155,57 @@ class TestAdminMergeObjects(TestCase): if event.organisation == self.organisations[3]: # The one we left in place continue self.assertEqual(updatedEvent.organisation, self.organisations[1]) + +class TestInvoiceDelete(TestCase): + @classmethod + def setUpTestData(cls): + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True) + + cls.events = { + 1: models.Event.objects.create(name="TE E1", start_date=date.today()), + 2: models.Event.objects.create(name="TE E2", start_date=date.today()) + } + + cls.invoices = { + 1: models.Invoice.objects.create(event=cls.events[1]), + 2: models.Invoice.objects.create(event=cls.events[2]) + } + + cls.payments = { + 1: models.Payment.objects.create(invoice=cls.invoices[1], date=date.today(), amount=12.34, method=models.Payment.CASH) + } + + def setUp(self): + self.profile.set_password('testuser') + self.profile.save() + self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + + def test_invoice_delete_allowed(self): + request_url = reverse('invoice_delete', kwargs={'pk':self.invoices[2].pk}) + + response = self.client.get(request_url, follow=True) + self.assertContains(response, "Are you sure") + + # Check the invoice still exists + self.assertTrue(models.Invoice.objects.get(pk=self.invoices[2].pk)) + + # Actually delete it + response = self.client.post(request_url, follow=True) + + # Check the invoice is deleted + self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk) + + def test_invoice_delete_not_allowed(self): + request_url = reverse('invoice_delete', kwargs={'pk':self.invoices[1].pk}) + + response = self.client.get(request_url, follow=True) + self.assertContains(response, "To delete an invoice, delete the payments first.") + + # Check the invoice still exists + self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk)) + + # Try to actually delete it + response = self.client.post(request_url, follow=True) + + # Check this didn't work + self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk)) \ No newline at end of file From 05b2de561e3c0a18c05e7f2fcbf0d9fa19bde33a Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 10 Jul 2016 09:54:35 +0100 Subject: [PATCH 47/67] Added phone links - closes #247 --- RIGS/templates/RIGS/organisation_list.html | 2 +- RIGS/templates/RIGS/person_list.html | 2 +- RIGS/templates/RIGS/profile_detail.html | 2 +- RIGS/templates/RIGS/venue_list.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RIGS/templates/RIGS/organisation_list.html b/RIGS/templates/RIGS/organisation_list.html index 2c88408a..c8856886 100644 --- a/RIGS/templates/RIGS/organisation_list.html +++ b/RIGS/templates/RIGS/organisation_list.html @@ -45,7 +45,7 @@
- + - + - + - +
{{ object.pk }} {{ object.name }} {{ object.email }}{{ object.phone }}{{ object.phone }} {{ object.notes|yesno|capfirst }} {{ object.union_account|yesno|capfirst }} diff --git a/RIGS/templates/RIGS/person_list.html b/RIGS/templates/RIGS/person_list.html index a7c1c08d..2cbdff8e 100644 --- a/RIGS/templates/RIGS/person_list.html +++ b/RIGS/templates/RIGS/person_list.html @@ -44,7 +44,7 @@ {{ person.pk }} {{ person.name }} {{ person.email }}{{ person.phone }}{{ person.phone }} {{ person.notes|yesno|capfirst }} diff --git a/RIGS/templates/RIGS/profile_detail.html b/RIGS/templates/RIGS/profile_detail.html index e69e7bf0..11904b18 100644 --- a/RIGS/templates/RIGS/profile_detail.html +++ b/RIGS/templates/RIGS/profile_detail.html @@ -71,7 +71,7 @@
{{object.initials}}
Phone
-
{{object.phone}}
+
{{object.phone}}
{% if not request.is_ajax %} {% if object.pk == user.pk %} diff --git a/RIGS/templates/RIGS/venue_list.html b/RIGS/templates/RIGS/venue_list.html index cf89686c..88ae61ac 100644 --- a/RIGS/templates/RIGS/venue_list.html +++ b/RIGS/templates/RIGS/venue_list.html @@ -45,7 +45,7 @@
{{ object.pk }} {{ object.name }} {{ object.email }}{{ object.phone }}{{ object.phone }} {{ object.notes|yesno|capfirst }} From 39d27d273050cec057b60ebef4d049713f971f06 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 10 Jul 2016 10:49:23 +0100 Subject: [PATCH 48/67] Basic invoice UI improvements - closes #232 --- RIGS/finance.py | 1 + RIGS/models.py | 4 +++ RIGS/templates/RIGS/event_detail.html | 42 +++++++++++++++++++------ RIGS/templates/RIGS/invoice_detail.html | 2 +- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/RIGS/finance.py b/RIGS/finance.py index 75de8bd9..5cc5c0c1 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -144,6 +144,7 @@ class InvoiceEvent(generic.View): if created: invoice.invoice_date = datetime.date.today() + messages.success(self.request, 'Invoice created successfully') return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk})) diff --git a/RIGS/models.py b/RIGS/models.py index 535aa42c..0650d81c 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -526,6 +526,10 @@ class Invoice(models.Model): def balance(self): return self.sum_total - self.payment_total + @property + def is_closed(self): + return self.balance == 0 or self.void + def __str__(self): return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) diff --git a/RIGS/templates/RIGS/event_detail.html b/RIGS/templates/RIGS/event_detail.html index 81658663..d4b089a9 100644 --- a/RIGS/templates/RIGS/event_detail.html +++ b/RIGS/templates/RIGS/event_detail.html @@ -25,9 +25,17 @@ class="hidden-xs">Duplicate {% if event.is_rig %} {% if perms.RIGS.add_invoice %} - + + {% endif %} {% endif %} @@ -190,9 +198,17 @@ class="hidden-xs">Duplicate {% if event.is_rig %} {% if perms.RIGS.add_invoice %} - + + {% endif %} {% endif %} @@ -227,9 +243,17 @@ class="hidden-xs">Duplicate {% if event.is_rig %} {% if perms.RIGS.add_invoice %} - + + {% endif %} {% endif %} diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html index ad177ba4..2973c358 100644 --- a/RIGS/templates/RIGS/invoice_detail.html +++ b/RIGS/templates/RIGS/invoice_detail.html @@ -38,7 +38,7 @@
-
+
Event Details
From 01d2eae7bcffd4c36684be2a15afec8da78fabd3 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sun, 10 Jul 2016 11:14:24 +0100 Subject: [PATCH 49/67] More invoice UI improvements - makes colouring consistent - also closes #242 --- RIGS/templates/RIGS/invoice_detail.html | 7 +++++-- RIGS/templates/RIGS/invoice_list.html | 19 +++++++++++++++++-- templates/base.html | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html index 2973c358..af26ad21 100644 --- a/RIGS/templates/RIGS/invoice_detail.html +++ b/RIGS/templates/RIGS/invoice_detail.html @@ -38,8 +38,11 @@
-
-
Event Details
+
+
Event Details + {% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %} + +
Event Number
diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html index 5bd7ef70..7dc6d8ee 100644 --- a/RIGS/templates/RIGS/invoice_list.html +++ b/RIGS/templates/RIGS/invoice_list.html @@ -29,8 +29,23 @@ {% for object in object_list %}
{{ object.pk }}N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
- {{ object.event.get_status_display }}
N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }}
+ {{ object.event.get_status_display }} + {% if not object.event.mic %}(No MIC){% endif %} +
{% if object.event.organisation %} {{ object.event.organisation.name }} diff --git a/templates/base.html b/templates/base.html index 7a2d15f9..24cc30bc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -76,9 +76,9 @@