Compare commits

..

20 Commits

Author SHA1 Message Date
Tom Price
c0c2314fbe Start building a page to select items based on category.
Requires some new CSS for the side nav.
2016-01-05 20:03:16 +00:00
Tom Price
d62fcc8483 Add training database index 2016-01-05 00:17:33 +00:00
Tom Price
b838d25cc2 Refactor training status table down to the simplest elements so they can be included as needed. 2016-01-05 00:07:22 +00:00
Tom Price
ee4bd832fa Merge branch 'login' into training 2016-01-04 23:58:01 +00:00
Tom Price
8c231b931d Deleted left over incorrect .idea/ files. 2015-12-23 01:07:36 +00:00
Tom Price
c5970d2542 A first view that shows a user their current training status. 2015-12-23 01:06:18 +00:00
Tom Price
1922992633 Merge branch 'master' into training 2015-12-22 23:08:02 +00:00
Tom Price
ec4e033745 Merge branch 'master' into training
# Conflicts:
#	.idea/dataSources.ids
#	db.sqlite3
2015-11-10 12:18:53 +00:00
Tom Price
9379050e15 Add basic detail view for training record.
Only committing because I have other things to do
2015-10-28 17:41:02 +00:00
Tom Price
4689775a5f Several changes to models
Add __str__ methods with unicode compatibility.

Make trainer fields optional as they should be.
Include these migrations and apply to DB.

Install some test data in the DB.
2015-10-22 13:54:26 +01:00
Tom Price
32aabe14c7 Add training models to admin interface 2015-10-22 13:40:42 +01:00
Tom Price
697a5977c8 Add the unique together constraint to TrainingRecord model and actually commit the migration this time. 2015-10-22 00:19:05 +01:00
Tom Price
80ba59c3ba Fix models and create migration 2015-10-21 17:51:59 +01:00
Tom Price
c60e3fb9fe Add training app to installed apps 2015-10-21 17:03:46 +01:00
Tom Price
9beb7c1612 Update to allow pycharm to access the local postgres db 2015-10-21 17:00:03 +01:00
Tom Price
227b63e689 Add a sensible data structure.
This is sensible to the current structure but there are a few changes. These were done to maintain the same functionality, but to reduce the number of DB rows used.
2015-10-21 16:59:17 +01:00
Tom Price
fa231887d2 Add local postgres database for training to settings 2015-10-21 16:08:17 +01:00
Tom Price
37e5549736 Convert the export scripts from generic SQL to usable postgres including relationships 2015-10-21 15:57:47 +01:00
Tom Price
adbcd0733d Add initial data exported from access 2015-10-21 13:52:33 +01:00
Tom Price
0b35965fa1 Start the conversion of the training database as a priority.
Also includes a script to export access to SQL
2015-10-21 13:32:25 +01:00
73 changed files with 93826 additions and 1274 deletions

1
.idea/.name generated
View File

@@ -1 +0,0 @@
PyRIGS

5
.idea/encodings.xml generated
View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
</modules>
</component>
</project>

View File

@@ -1,5 +0,0 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

7
.idea/vcs.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -58,7 +58,7 @@ def api_key_required(function):
try: try:
user_object = models.Profile.objects.get(pk=userid) user_object = models.Profile.objects.get(pk=userid)
except models.Profile.DoesNotExist: except Profile.DoesNotExist:
return error_resp return error_resp
if user_object.api_key != key: if user_object.api_key != key:

View File

@@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
import os import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
@@ -25,10 +27,6 @@ TEMPLATE_DEBUG = True
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] 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'] INTERNAL_IPS = ['127.0.0.1']
ADMINS = ( ADMINS = (
@@ -46,7 +44,7 @@ INSTALLED_APPS = (
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'RIGS', 'RIGS',
'subhire', 'training',
'debug_toolbar', 'debug_toolbar',
'registration', 'registration',
@@ -58,7 +56,6 @@ INSTALLED_APPS = (
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware', 'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
'reversion.middleware.RevisionMiddleware', 'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@@ -80,6 +77,14 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
},
# Legacy training database
'training': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'HOST': 'localhost',
'NAME': 'tec_training',
'USER': 'pyrigs',
'PASSWORD': 'pyrigs',
} }
} }

View File

@@ -12,7 +12,8 @@ urlpatterns = patterns('',
# url(r'^blog/', include('blog.urls')), # url(r'^blog/', include('blog.urls')),
url(r'^', include('RIGS.urls')), url(r'^', include('RIGS.urls')),
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail), url('^training/', include('training.urls', namespace='training')),
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
name="registration_register"), name="registration_register"),
url('^user/', include('django.contrib.auth.urls')), url('^user/', include('django.contrib.auth.urls')),
url('^user/', include('registration.backends.default.urls')), url('^user/', include('registration.backends.default.urls')),

View File

@@ -1,5 +1,5 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
[![wercker status](https://app.wercker.com/status/2dbe0517c3d83859c985ffc5a55a2802/m/master "wercker status")](https://app.wercker.com/project/bykey/2dbe0517c3d83859c985ffc5a55a2802) [![wercker status](https://app.wercker.com/status/b26100ecccdfb46a9a9056553daac5b7/m/master "wercker status")](https://app.wercker.com/project/bykey/b26100ecccdfb46a9a9056553daac5b7)
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
@@ -75,4 +75,4 @@ python manage.py runserver
Please refer to Django documentation for a full list of options available here. Please refer to Django documentation for a full list of options available here.
### Committing, pushing and testing ### ### Committing, pushing and testing ###
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person. Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.

View File

@@ -4,32 +4,25 @@ from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import reversion 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
from django.db.models import Count
from django.forms import ModelForm
# Register your models here. # 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.VatRate, reversion.VersionAdmin)
admin.site.register(models.Venue, reversion.VersionAdmin)
admin.site.register(models.Event, reversion.VersionAdmin) admin.site.register(models.Event, reversion.VersionAdmin)
admin.site.register(models.EventItem, reversion.VersionAdmin) admin.site.register(models.EventItem, reversion.VersionAdmin)
admin.site.register(models.Invoice) admin.site.register(models.Invoice)
admin.site.register(models.Payment) admin.site.register(models.Payment)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin): class ProfileAdmin(UserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), { (_('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', (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}), 'groups', 'user_permissions')}),
(_('Important dates'), { (_('Important dates'), {
'fields': ('last_login', 'date_joined')}), 'fields': ('last_login', 'date_joined')}),
) )
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
@@ -40,76 +33,4 @@ class ProfileAdmin(UserAdmin):
form = forms.ProfileChangeForm form = forms.ProfileChangeForm
add_form = forms.ProfileCreationForm add_form = forms.ProfileCreationForm
admin.site.register(models.Profile, ProfileAdmin)
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):
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()
reversion.set_comment('Merging Objects')
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)
@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']

View File

@@ -1,41 +1,32 @@
import cStringIO as StringIO import cStringIO as StringIO
import datetime
import re
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.db import connection
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse from django.views import generic
from django.shortcuts import get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import get_template from django.template.loader import get_template
from django.views import generic from django.http import HttpResponse
from django.db.models import Q from django.shortcuts import get_object_or_404
from django.contrib import messages
import datetime
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from RIGS import models from RIGS import models
import re
class InvoiceIndex(generic.ListView): class InvoiceIndex(generic.ListView):
model = models.Invoice model = models.Invoice
template_name = 'RIGS/invoice_list_active.html' 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
context['count'] = len(list(context['object_list']))
return context
def get_queryset(self): def get_queryset(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must # Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \ sql = "SELECT * FROM " \
"(SELECT " \ "(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(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\") " \ "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \ "AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \ "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
@@ -49,7 +40,6 @@ class InvoiceIndex(generic.ListView):
class InvoiceDetail(generic.DetailView): class InvoiceDetail(generic.DetailView):
model = models.Invoice model = models.Invoice
class InvoicePrint(generic.View): class InvoicePrint(generic.View):
def get(self, request, pk): def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk) invoice = get_object_or_404(models.Invoice, pk=pk)
@@ -64,8 +54,8 @@ class InvoicePrint(generic.View):
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF', 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
} }
}, },
'invoice': invoice, 'invoice':invoice,
'current_user': request.user, 'current_user':request.user,
}) })
rml = template.render(context) rml = template.render(context)
@@ -82,7 +72,6 @@ class InvoicePrint(generic.View):
response.write(pdfData) response.write(pdfData)
return response return response
class InvoiceVoid(generic.View): class InvoiceVoid(generic.View):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
pk = kwargs.get('pk') pk = kwargs.get('pk')
@@ -97,7 +86,6 @@ class InvoiceVoid(generic.View):
class InvoiceArchive(generic.ListView): class InvoiceArchive(generic.ListView):
model = models.Invoice model = models.Invoice
template_name = 'RIGS/invoice_list_archive.html'
paginate_by = 25 paginate_by = 25
@@ -106,33 +94,14 @@ class InvoiceWaiting(generic.ListView):
paginate_by = 25 paginate_by = 25
template_name = 'RIGS/event_invoice.html' 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 self.get_objects():
total += obj.sum_total
context['total'] = total
context['count'] = len(self.get_objects())
return context
def get_queryset(self): def get_queryset(self):
return self.get_objects()
def get_objects(self):
# @todo find a way to select items # @todo find a way to select items
events = self.model.objects.filter( events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(),
( invoice__isnull=True) \
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end .order_by('start_date') \
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', .select_related('person',
'organisation', 'organisation',
'venue', 'mic') \ 'venue', 'mic')
.prefetch_related('items')
return events return events
@@ -150,7 +119,7 @@ class InvoiceEvent(generic.View):
class PaymentCreate(generic.CreateView): class PaymentCreate(generic.CreateView):
model = models.Payment model = models.Payment
fields = ['invoice', 'date', 'amount', 'method'] fields = ['invoice','date','amount','method']
def get_initial(self): def get_initial(self):
initial = super(generic.CreateView, self).get_initial() initial = super(generic.CreateView, self).get_initial()
@@ -170,4 +139,4 @@ class PaymentDelete(generic.DeleteView):
model = models.Payment model = models.Payment
def get_success_url(self): def get_success_url(self):
return self.request.POST.get('next') return self.request.POST.get('next')

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0023_auto_20150529_0048'),
]
operations = [
migrations.AlterField(
model_name='event',
name='based_on',
field=models.ForeignKey(related_name='future_events', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='RIGS.Event', null=True),
),
]

View File

@@ -1,32 +1,31 @@
import datetime
import hashlib import hashlib
import pytz import datetime, pytz
import random
import string
from collections import Counter
from decimal import Decimal
import reversion from django.db import models, connection
from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.conf import settings
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 from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
import reversion
import string
import random
from collections import Counter
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import ValidationError
from decimal import Decimal
# Create your models here. # Create your models here.
@python_2_unicode_compatible @python_2_unicode_compatible
class Profile(AbstractUser): class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True) 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 @classmethod
def make_api_key(cls): def make_api_key(cls):
size = 20 size=20
chars = string.ascii_letters + string.digits chars=string.ascii_letters + string.digits
new_api_key = ''.join(random.choice(chars) for x in range(size)) new_api_key = ''.join(random.choice(chars) for x in range(size))
return new_api_key; return new_api_key;
@@ -56,7 +55,6 @@ class Profile(AbstractUser):
('view_profile', 'Can view Profile'), ('view_profile', 'Can view Profile'),
) )
class RevisionMixin(object): class RevisionMixin(object):
@property @property
def last_edited_at(self): def last_edited_at(self):
@@ -81,11 +79,10 @@ class RevisionMixin(object):
versions = reversion.get_for_object(self) versions = reversion.get_for_object(self)
if versions: if versions:
version = reversion.get_for_object(self)[0] 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: else:
return None return None
@reversion.register @reversion.register
@python_2_unicode_compatible @python_2_unicode_compatible
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
@@ -100,7 +97,7 @@ class Person(models.Model, RevisionMixin):
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes is not None: if self.notes is not None:
if len(self.notes) > 0: if len(self.notes) > 0:
string += "*" string += "*"
return string return string
@@ -111,7 +108,7 @@ class Person(models.Model, RevisionMixin):
if e.organisation: if e.organisation:
o.append(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) c = Counter(o)
stats = c.most_common() stats = c.most_common()
return stats return stats
@@ -144,7 +141,7 @@ class Organisation(models.Model, RevisionMixin):
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes is not None: if self.notes is not None:
if len(self.notes) > 0: if len(self.notes) > 0:
string += "*" string += "*"
return string return string
@@ -154,8 +151,8 @@ class Organisation(models.Model, RevisionMixin):
for e in Event.objects.filter(organisation=self).select_related('person'): for e in Event.objects.filter(organisation=self).select_related('person'):
if e.person: if e.person:
p.append(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) c = Counter(p)
stats = c.most_common() stats = c.most_common()
return stats return stats
@@ -241,18 +238,12 @@ class Venue(models.Model, RevisionMixin):
class EventManager(models.Manager): class EventManager(models.Manager):
def current_events(self): def current_events(self):
events = self.filter( events = self.filter(
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q( (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
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(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q( (models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire
status=Event.CANCELLED)) | # Ends after (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(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 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', ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
'organisation',
'venue', 'mic')
return events return events
def events_in_bounds(self, start, end): def events_in_bounds(self, start, end):
@@ -260,17 +251,15 @@ class EventManager(models.Manager):
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds (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(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(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(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, start_date__gte=end)) | # Access before, start after
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end 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, start_date__gte=end)) | # Meet before, start after
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end 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', ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
'organisation',
'venue', 'mic')
return events return events
def rig_count(self): def rig_count(self):
@@ -312,8 +301,7 @@ class Event(models.Model, RevisionMixin):
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False) dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True) is_rig = models.BooleanField(default=True)
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, based_on = models.ForeignKey('Event', related_name='future_events', blank=True, null=True)
null=True)
# Timing # Timing
start_date = models.DateField() start_date = models.DateField()
@@ -339,7 +327,6 @@ class Event(models.Model, RevisionMixin):
""" """
EX Vat EX Vat
""" """
@property @property
def sum_total(self): def sum_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic # Manual querying is required for efficiency whilst maintaining floating point arithmetic
@@ -347,15 +334,14 @@ class Event(models.Model, RevisionMixin):
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id # sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
# else: # else:
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id # sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
# total = self.items.raw(sql)[0] #total = self.items.raw(sql)[0]
# if total.sum_total: #if total.sum_total:
# return total.sum_total # return total.sum_total
# total = 0.0 #total = 0.0
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"): #for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total += item.sum # total += item.sum
total = EventItem.objects.filter(event=self).aggregate( total = EventItem.objects.filter(event=self).aggregate(
sum_total=models.Sum(models.F('cost') * models.F('quantity'), sum_total=models.Sum(models.F('cost')*models.F('quantity'), output_field=models.DecimalField(max_digits=10, decimal_places=2))
output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total'] )['sum_total']
if total: if total:
return total return total
@@ -372,7 +358,6 @@ class Event(models.Model, RevisionMixin):
""" """
Inc VAT Inc VAT
""" """
@property @property
def total(self): def total(self):
return self.sum_total + self.vat return self.sum_total + self.vat
@@ -397,7 +382,7 @@ class Event(models.Model, RevisionMixin):
def earliest_time(self): 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""" """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 = [] datetime_list = []
if self.access_at: if self.access_at:
@@ -409,22 +394,22 @@ class Event(models.Model, RevisionMixin):
# If there is no start time defined, pretend it's midnight # If there is no start time defined, pretend it's midnight
startTimeFaked = False startTimeFaked = False
if self.has_start_time: 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: 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 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) tz = pytz.timezone(settings.TIME_ZONE)
startDateTime = tz.localize(startDateTime) 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 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 self.start_date
return earliest return earliest
@property @property
@@ -436,7 +421,7 @@ class Event(models.Model, RevisionMixin):
endDate = self.start_date endDate = self.start_date
if self.has_end_time: 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) tz = pytz.timezone(settings.TIME_ZONE)
endDateTime = tz.localize(endDateTime) endDateTime = tz.localize(endDateTime)
@@ -445,6 +430,7 @@ class Event(models.Model, RevisionMixin):
else: else:
return endDate return endDate
objects = EventManager() objects = EventManager()
def get_absolute_url(self): def get_absolute_url(self):
@@ -460,7 +446,7 @@ class Event(models.Model, RevisionMixin):
startEndSameDay = not self.end_date or self.end_date == self.start_date startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.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): def save(self, *args, **kwargs):
"""Call :meth:`full_clean` before saving.""" """Call :meth:`full_clean` before saving."""
@@ -517,6 +503,15 @@ class Invoice(models.Model):
@property @property
def payment_total(self): 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'] total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
if total: if total:
return total return total
@@ -557,4 +552,4 @@ class Payment(models.Model):
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True) method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
def __str__(self): def __str__(self):
return "%s: %d" % (self.get_method_display(), self.amount) return "%s: %d" % (self.get_method_display(), self.amount)

View File

@@ -37,12 +37,6 @@ class RigboardIndex(generic.TemplateView):
class WebCalendar(generic.TemplateView): class WebCalendar(generic.TemplateView):
template_name = 'RIGS/calendar.html' template_name = 'RIGS/calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
context['view'] = kwargs.get('view','')
context['date'] = kwargs.get('date','')
return context
class EventDetail(generic.DetailView): class EventDetail(generic.DetailView):
model = models.Event model = models.Event

View File

@@ -11,7 +11,6 @@ fonts_dir = "fonts"
# You can select your preferred output style here (can be overridden via the command line): # You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed # output_style = :expanded or :nested or :compact or :compressed
output_style = :compressed
# To enable relative paths to assets via compass helper functions. Uncomment: # To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true # relative_assets = true

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,7 @@ var Konami = function (callback) {
return false; return false;
} }
}, this); }, this);
/*this.iphone.load(link);*/ this.iphone.load(link);
}, },
code: function (link) { code: function (link) {
window.location = link window.location = link

View File

@@ -75,16 +75,6 @@ textarea {
} }
} }
del {
background-color: #f2dede;
border-radius: 3px;
}
ins {
background-color: #dff0d8;
border-radius: 3px;
}
.loading-animation { .loading-animation {
position: relative; position: relative;
margin: 30px auto 0; margin: 30px auto 0;
@@ -147,3 +137,20 @@ ins {
}; };
} }
} }
.toc-nav {
width: auto;
display: inline-block;
&.affix {
top: $navbar-height;
}
}
.anchor {
display: block;
position: relative;
top: -$navbar-height - 10px;
visibility: hidden;
}

View File

@@ -40,9 +40,6 @@
{% endif %} {% endif %}
{% include 'RIGS/object_button.html' with object=version.new %} {% include 'RIGS/object_button.html' with object=version.new %}
{% if version.revision.comment %}
({{ version.revision.comment }})
{% endif %}
</small> </small>
</p> </p>

View File

@@ -59,7 +59,6 @@
<td>Version ID</td> <td>Version ID</td>
<td>User</td> <td>User</td>
<td>Changes</td> <td>Changes</td>
<td>Comment</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -72,11 +71,10 @@
<td>{{ version.revision.user.name }}</td> <td>{{ version.revision.user.name }}</td>
<td> <td>
{% if version.old == None %} {% if version.old == None %}
{{version.new|to_class_name}} Created Object Created
{% else %} {% else %}
{% include 'RIGS/version_changes.html' %} {% include 'RIGS/version_changes.html' %}
{% endif %} </td> {% endif %} </td>
<td>{{ version.revision.comment }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -1,40 +0,0 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% block content %}
<form action="" method="post">{% csrf_token %}
<p>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.</p>
<table>
{% for form in forms %}
{% if forloop.first %}
<tr>
<th></th>
<th> ID </th>
{% for field in form %}
<th>{{ field.label }}</th>
{% endfor %}
</tr>
{% endif %}
<tr>
<td><input type="radio" name="master" value="{{form.instance.pk|unlocalize}}"></td>
<td>{{form.instance.pk}}</td>
{% for field in form %}
<td> {{ field.value }} </td>
{% endfor %}
</tr>
{% endfor %}
</table>
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="merge" />
<input type="hidden" name="post" value="yes" />
<input type="submit" value="Merge them" />
</div>
</form>
{% endblock %}

View File

@@ -14,28 +14,8 @@
<script src="{% static "js/moment.min.js" %}"></script> <script src="{% static "js/moment.min.js" %}"></script>
<script src="{% static "js/fullcalendar.js" %}"></script> <script src="{% static "js/fullcalendar.js" %}"></script>
<script> <script>
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
$(document).ready(function() { $(document).ready(function() {
viewToUrl = {
'agendaWeek':'week',
'agendaDay':'day',
'month':'month'
}
viewFromUrl = {
'week':'agendaWeek',
'day':'agendaDay',
'month':'month'
}
$('#calendar').fullCalendar({ $('#calendar').fullCalendar({
editable: false, editable: false,
eventLimit: true, // allow "more" link when too many events eventLimit: true, // allow "more" link when too many events
@@ -134,11 +114,8 @@
$('#day-button').addClass('active'); $('#day-button').addClass('active');
break; break;
} }
history.replaceState(null,null,'{% url 'web_calendar' %}'+viewToUrl[view.name]+'/'+view.intervalStart.format('YYYY-MM-DD')+'/');
} }
}); });
// set some button listeners // set some button listeners
@@ -169,18 +146,6 @@
} }
}); });
// Go to the initial settings, if they're valid
view = viewFromUrl['{{view}}'];
$('#calendar').fullCalendar( 'changeView', view);
day = moment('{{date}}');
if(day.isValid()){
$('#calendar').fullCalendar( 'gotoDate', day);
}else{
console.log('Supplied date is invalid - using default')
}
}); });
</script> </script>

View File

@@ -151,7 +151,7 @@
<a href="{% url 'event_detail' pk=object.based_on.pk %}"> <a href="{% url 'event_detail' pk=object.based_on.pk %}">
{% if object.based_on.is_rig %}N{{ object.based_on.pk|stringformat:"05d" }}{% else %} {% if object.based_on.is_rig %}N{{ object.based_on.pk|stringformat:"05d" }}{% else %}
{{ object.based_on.pk }}{% endif %} {{ object.based_on.pk }}{% endif %}
{{ object.based_on.name }} {% if object.based_on.mic %}by {{ object.based_on.mic.name }}{% endif %} {{ object.base_on.name }} by {{ object.based_on.mic.name }}
</a> </a>
{% endif %} {% endif %}
</dd> </dd>
@@ -233,17 +233,13 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div>
{% endif %}
{% endif %}
{% if not request.is_ajax %}
<div class="col-sm-12 text-right">
<div> <div>
<a href="{% url 'event_history' object.pk %}" title="View Revision History"> <a href="{% url 'event_history' object.pk %}" title="View Revision History">
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }} Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
</a> </a>
</div> </div>
</div> </div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -29,9 +29,9 @@
} }
function setTime02Hours() { function setTime02Hours() {
var id_start = "{{ form.start_date.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_date = "{{ form.end_date.id_for_label }}"
var id_end_time = "{{ form.end_time.id_for_label }}"; var id_end_time = "{{ form.end_time.id_for_label }}"
if ($('#'+id_start).val() == $('#'+id_end_date).val()) { if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
var end_date = new Date($('#'+id_end_date).val()); var end_date = new Date($('#'+id_end_date).val());
end_date.setDate(end_date.getDate() + 1); end_date.setDate(end_date.getDate() + 1);
@@ -63,12 +63,11 @@
} else { } else {
$('.form-is_rig').slideDown(); $('.form-is_rig').slideDown();
} }
$('.form-hws').css('overflow', 'visible');
} else { } else {
$('#{{form.is_rig.auto_id}}').prop('checked', false); $('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp(); $('.form-is_rig').slideUp();
} }
}); })
{% endif %} {% endif %}
function supportsDate() { function supportsDate() {
@@ -107,7 +106,7 @@
}); });
} }
}); })
$(document).ready(function () { $(document).ready(function () {
setupItemTable($("#{{ form.items_json.id_for_label }}").val()); setupItemTable($("#{{ form.items_json.id_for_label }}").val());
@@ -151,12 +150,10 @@
<div class="col-md-12 well"> <div class="col-md-12 well">
<div class="form-group" id="is_rig-selector"> <div class="form-group" id="is_rig-selector">
<div class="col-sm-12"> <div class="col-sm-12">
<span class="col-sm-6" data-toggle="tooltip" <span class="col-sm-6">
title="Anything that involves TEC kit, crew, or otherwise us providing a service to anyone.">
<button type="button" class="btn btn-primary col-xs-12" data-is_rig="1">Rig</button> <button type="button" class="btn btn-primary col-xs-12" data-is_rig="1">Rig</button>
</span> </span>
<span class="col-sm-6" data-toggle="tooltip" <span class="col-sm-6">
title="Things that aren't service-based, like training, meetings and site visits.">
<button type="button" class="btn btn-info col-xs-12" data-is_rig="0">Non-Rig</button> <button type="button" class="btn btn-info col-xs-12" data-is_rig="0">Non-Rig</button>
</span> </span>
</div> </div>

View File

@@ -1,91 +1,66 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load paginator from filters %} {% load paginator from filters %}
{% load static %}
{% block title %}Events for Invoice{% endblock %} {% block title %}Events for Invoice{% endblock %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h2>Events for Invoice ({{count}} Events, £ {{ total|floatformat:2 }})</h2> <h2>Events for Invoice</h2>
<p>These events have happened, but paperwork has not yet been sent to treasury</p>
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}
</div> </div>
{% endif %} {% endif %}
<div class="table-responsive col-sm-12"> <table class="table table-responsive table-hover">
<table class="table table-hover"> <thead>
<thead> <tr>
<tr> <th class="hiddenx-xs">#</th>
<th>#</th> <th>Date</th>
<th>Start Date</th> <th>Event</th>
<th>Event Name</th> <th>Client</th>
<th>Client</th> <th>Cost</th>
<th>Cost</th> <th class="hidden-xs">MIC</th>
<th>MIC</th> <th></th>
<th></th> </tr>
</thead>
<tbody>
{% for object in object_list %}
<tr class="
{% if event.cancelled %}
active
{% elif event.confirmed and event.mic or not event.is_rig %}
{# interpreated as (booked and mic) or is non rig #}
success
{% elif event.mic %}
warning
{% else %}
danger
{% endif %}
">
<td class="hidden-xs"><a href="{% url 'event_detail' object.pk %}" target="_blank">N{{ object.pk|stringformat:"05d" }}</a></td>
<td>{{ object.end_date }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.organisation %}
{{ object.organisation.name }}
{% else %}
{{ object.person.name }}
{% endif %}
</td>
<td>{{ object.sum_total|floatformat:2 }}</td>
<td class="text-center">
{{ object.mic.initials }}<br/>
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
</td>
<td class="text-right">
<a href="{% url 'invoice_event' object.pk %}" target="_blank" class="btn btn-default">
<span class="glyphicon glyphicon-gbp"></span>
</a>
</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for object in object_list %} </table>
<tr class="
{% if object.cancelled %}
active text-muted
{% elif not object.is_rig %}
info
{% elif object.confirmed and object.mic %}
{# interpreated as (booked and mic) #}
success
{% elif object.mic %}
warning
{% else %}
danger
{% endif %}
">
<td><a href="{% url 'event_detail' object.pk %}">N{{ object.pk|stringformat:"05d" }}</a><br>
<span class="text-muted">{{ object.get_status_display }}</span></td>
<td>{{ object.start_date }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.organisation %}
{{ object.organisation.name }}
<br>
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.person.name }}
<br>
<span class="text-muted">External</span>
{% endif %}
</td>
<td>{{ object.sum_total|floatformat:2 }}</td>
<td class="text-center">
{% if object.mic %}
{{ object.mic.initials }}<br>
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
{% else %}
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'invoice_event' object.pk %}" class="btn btn-default" data-toggle="tooltip" title="'Invoice' this event - click this when paperwork has been sent to treasury">
<span class="glyphicon glyphicon-gbp"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}

View File

@@ -189,7 +189,7 @@
<keepTogether> <keepTogether>
<blockTable style="totalTable" colWidths="300,115,80"> <blockTable style="totalTable" colWidths="300,115,80">
<tr> <tr>
<td>{% if not invoice %}VAT Registration Number: 170734807{% endif %}</td> <td>{% if not invoice %}VAT Registration Number: 116252989{% endif %}</td>
<td>Total (ex. VAT)</td> <td>Total (ex. VAT)</td>
<td>£ {{ object.sum_total|floatformat:2 }}</td> <td>£ {{ object.sum_total|floatformat:2 }}</td>
</tr> </tr>
@@ -209,7 +209,7 @@
<para> <para>
{% if invoice %} {% if invoice %}
VAT Registration Number: 170734807 VAT Registration Number: 116252989
{% else %} {% else %}
<b>This contract is not an invoice.</b> <b>This contract is not an invoice.</b>
{% endif %} {% endif %}

View File

@@ -1,7 +1,7 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
<td>#</td> <td class="hidden-xs">#</td>
<td>Event Date</td> <td>Event Date</td>
<td>Event Details</td> <td>Event Details</td>
<td>Event Timings</td> <td>Event Timings</td>
@@ -23,7 +23,7 @@
danger danger
{% endif %} {% endif %}
"> ">
<td>{{ event.pk }}</td> <td class="hidden-xs">{{ event.pk }}</td>
<td> <td>
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div> <div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
{% if event.end_date and event.end_date != event.start_date %} {% if event.end_date and event.end_date != event.start_date %}

View File

@@ -109,11 +109,6 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -5,56 +5,38 @@
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h2>{% block heading %}Invoices{% endblock %}</h2> <h2>Invoices</h2>
{% block description %}{% endblock %}
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}
</div> </div>
{% endif %} {% endif %}
<div class="table-responsive col-sm-12"> <table class="table table-responsive table-hover">
<table class="table table-hover"> <thead>
<thead> <tr>
<tr> <th>#</th>
<th>#</th> <th>Event</th>
<th>Event</th> <th>Invoice Date</th>
<th>Client</th> <th>Balance</th>
<th>Event Date</th> <th></th>
<th>Invoice Date</th> </tr>
<th>Balance</th> </thead>
<th></th> <tbody>
{% for object in object_list %}
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
<td>{{ object.pk }}</td>
<td><a href="{% url 'event_detail' object.event.pk %}" target="_blank">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }}</td>
<td>{{ object.invoice_date }}</td>
<td>{{ object.balance|floatformat:2 }}</td>
<td class="text-right">
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for object in object_list %} </table>
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
<td>{{ object.pk }}</td>
<td><a href="{% url 'event_detail' object.event.pk %}">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }} <br>
<span class="text-muted">{{ object.event.get_status_display }}</span></td>
</td>
<td>{% if object.event.organisation %}
{{ object.event.organisation.name }}
<br>
<span class="text-muted">{{ object.event.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.event.person.name }}
<br>
<span class="text-muted">External</span>
{% endif %}
</td>
<td>{{ object.event.start_date }}</td>
<td>{{ object.invoice_date }}</td>
<td>{{ object.balance|floatformat:2 }}</td>
<td class="text-right">
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}

View File

@@ -1,13 +0,0 @@
{% extends 'RIGS/invoice_list.html' %}
{% block title %}
Outstanding Invoices
{% endblock %}
{% block heading %}
Outstanding Invoices ({{ count }} Events, £ {{ total|floatformat:2 }})
{% endblock %}
{% block description %}
<p>Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger</p>
{% endblock %}

View File

@@ -1,13 +0,0 @@
{% extends 'RIGS/invoice_list.html' %}
{% block title %}
Invoice Archive
{% endblock %}
{% block heading %}
All Invoices
{% endblock %}
{% block description %}
<p>This page displays all invoices: outstanding, paid, and void</p>
{% endblock %}

View File

@@ -126,7 +126,7 @@
<dd> <dd>
{% if user.api_key %} {% if user.api_key %}
<pre id="cal-url" data-url="http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}"></pre> <pre id="cal-url" data-url="http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}"></pre>
<small><a id="gcal-link" data-url="https://support.google.com/calendar/answer/37100" href="">Click here</a> for instructions on adding to google calendar.<br/> <small><a id="gcal-link" data-url="http://www.google.com/calendar/render?cid=http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}" href="">Click here</a> to add to google calendar.<br/>
To sync from google calendar to mobile device, visit <a href="https://www.google.com/calendar/syncselect" target="_blank">this page</a> on your device and tick "RIGS Calendar".</small> To sync from google calendar to mobile device, visit <a href="https://www.google.com/calendar/syncselect" target="_blank">this page</a> on your device and tick "RIGS Calendar".</small>
{% else %} {% else %}
<pre>No API Key Generated</pre> <pre>No API Key Generated</pre>

View File

@@ -1,21 +1,40 @@
{% for change in version.field_changes %} {% for change in version.field_changes %}
<button title="Changes to {{ change.field.verbose_name }}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %} <button title="Changes to {{ change.field.verbose_name }}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='
{% include "RIGS/version_changes_change.html" %}
{% endspaceless %}'>{{ change.field.verbose_name }}</button> {% if change.new %}
<div class="alert alert-success {% if change.long %}overflow-ellipsis{% endif %}">
{% if change.linebreaks %}
{{change.new|linebreaksbr}}
{% else %}
{{change.new}}
{% endif %}
</div>
{% endif %}
{% if change.old %}
<div class="alert alert-danger {% if change.long %}overflow-ellipsis{% endif %}">
{% if change.linebreaks %}
{{change.old|linebreaksbr}}
{% else %}
{{change.old}}
{% endif %}
</div>
{% endif %}
'>{{ change.field.verbose_name }}</button>
{% endfor %} {% endfor %}
{% for itemChange in version.item_changes %} {% for itemChange in version.item_changes %}
<button title="Changes to item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %} <button title="Changes to item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='
<ul class="list-group">
{% for change in itemChange.changes %} {% for change in itemChange.changes %}
<li class="list-group-item"> <h4>{{ change.field.verbose_name }}</h4>
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
{% include "RIGS/version_changes_change.html" %} {% if change.new %}<div class="alert alert-success">{{change.new|linebreaksbr}}</div>{% endif %}
</li> {% if change.old %}<div class="alert alert-danger">{{change.old|linebreaksbr}}</div>{% endif %}
{% endfor %} {% endfor %}
</ul>
{% endspaceless %}'>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button> '>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button>
{% endfor %} {% endfor %}

View File

@@ -1,34 +0,0 @@
{# pass in variable "change" to this template #}
{% if change.linebreaks and change.new and change.old %}
{% for diff in change.diff %}
{% if diff.type == "insert" %}
<ins>{{ diff.text|linebreaksbr }}</ins>
{% elif diff.type == "delete" %}
<del>{{diff.text|linebreaksbr}}</del>
{% else %}
<span>{{diff.text|linebreaksbr}}</span>
{% endif %}
{% endfor %}
{% else %}
{% if change.old %}
<del {% if change.long %}class="overflow-ellipsis"{% endif %}>
{% if change.linebreaks %}
{{change.old|linebreaksbr}}
{% else %}
{{change.old}}
{% endif %}
</del>
{% endif %}
{% if change.new and change.old %}
<br/>
{% endif %}
{% if change.new %}
<ins {% if change.long %}class="overflow-ellipsis"{% endif %}>
{% if change.linebreaks %}
{{change.new|linebreaksbr}}
{% else %}
{{change.new}}
{% endif %}
</ins>
{% endif %}
{% endif %}

View File

@@ -35,7 +35,6 @@
<td>Version ID</td> <td>Version ID</td>
<td>User</td> <td>User</td>
<td>Changes</td> <td>Changes</td>
<td>Comment</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -47,14 +46,11 @@
<td>{{ version.revision.user.name }}</td> <td>{{ version.revision.user.name }}</td>
<td> <td>
{% if version.old == None %} {% if version.old == None %}
{{object|to_class_name}} Created Object Created
{% else %} {% else %}
{% include 'RIGS/version_changes.html' %} {% include 'RIGS/version_changes.html' %}
{% endif %} {% endif %}
</td> </td>
<td>
{{ version.revision.comment }}
</td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@@ -4,7 +4,7 @@ from django.test.client import Client
from django.core import mail from django.core import mail
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from RIGS import models from RIGS import models
import re import re
@@ -159,7 +159,6 @@ class EventTest(LiveServerTestCase):
self.browser = webdriver.Firefox() self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3) # Set implicit wait session wide self.browser.implicitly_wait(3) # Set implicit wait session wide
self.browser.maximize_window()
os.environ['RECAPTCHA_TESTING'] = 'True' os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self): def tearDown(self):
@@ -196,245 +195,233 @@ class EventTest(LiveServerTestCase):
self.browser.get(self.live_server_url + '/rigboard/') self.browser.get(self.live_server_url + '/rigboard/')
def testRigCreate(self): def testRigCreate(self):
# Requests address
self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
# Check has slided up correctly - second save button hidden
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
self.assertFalse(save.is_displayed())
# Click Rig button
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
# Slider expands and save button visible
self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form')
# Create new person
add_person_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "add")]')
add_person_button.click()
# See modal has opened
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
# Fill person form out and submit
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
# See new person selected
person1 = models.Person.objects.get(name="Test Person 1")
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Change mind and add another
add_person_button.click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person2 = models.Person.objects.get(name="Test Person 2")
self.assertEqual(person2.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Have to do this explcitly to force the wait for it to update
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person2.pk, int(option.get_attribute("value")))
# Was right the first time, change it back
person_select = form.find_element_by_xpath(
'//button[@data-id="id_person"]')
person_select.send_keys(person1.name)
person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name)
person_dropped.click()
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Edit Person 1 to have a better name
form.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
name = modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]')
self.assertEqual(person1.name, name.get_attribute('value'))
name.clear()
name.send_keys('Rig ' + person1.name)
name.send_keys(Keys.ENTER)
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person1 = models.Person.objects.get(pk=person1.pk)
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Create organisation
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Organisation.objects.get(name="Test Organisation")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_organisation"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_organisation"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Create veneue
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_venue" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Venue.objects.get(name="Test Venue")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_venue"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_venue"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('3015-05-25')
form.find_element_by_id('id_start_time').send_keys('06:59')
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('4000-06-27')
form.find_element_by_id('id_end_time').send_keys('07:00')
# Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 1")
modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
e.send_keys(Keys.ENTER) # enter submit
# Confirm item has been saved to json field
objectitems = self.browser.execute_script("return objectitems;")
self.assertEqual(1, len(objectitems))
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
# See new item appear in table
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
self.assertIn("This is an item description", row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual(u'£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
self.assertEqual(u'£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# Attempt to save - missing title
save.click()
# See error
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
self.assertTrue(error.is_displayed())
# Should only have one error message
self.assertEqual("Name", error.find_element_by_xpath('//dt[1]').text)
self.assertEqual("This field is required.", error.find_element_by_xpath('//dd[1]/ul/li').text)
# don't need error so close it
error.find_element_by_xpath('//div[contains(@class, "alert-danger")]//button[@class="close"]').click()
try: try:
# Requests address self.assertFalse(error.is_displayed())
self.browser.get(self.live_server_url + '/event/create/') except StaleElementReferenceException:
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
# Check has slided up correctly - second save button hidden
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
self.assertFalse(save.is_displayed())
# Click Rig button
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
# Slider expands and save button visible
self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form')
# Create new person
wait.until(animation_is_finished())
add_person_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "add")]')
add_person_button.click()
# See modal has opened
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
# Fill person form out and submit
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
# See new person selected
person1 = models.Person.objects.get(name="Test Person 1")
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Change mind and add another
wait.until(animation_is_finished())
add_person_button.click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person2 = models.Person.objects.get(name="Test Person 2")
self.assertEqual(person2.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Have to do this explcitly to force the wait for it to update
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person2.pk, int(option.get_attribute("value")))
# Was right the first time, change it back
person_select = form.find_element_by_xpath(
'//button[@data-id="id_person"]')
person_select.send_keys(person1.name)
person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name)
person_dropped.click()
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Edit Person 1 to have a better name
form.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
name = modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]')
self.assertEqual(person1.name, name.get_attribute('value'))
name.clear()
name.send_keys('Rig ' + person1.name)
name.send_keys(Keys.ENTER)
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person1 = models.Person.objects.get(pk=person1.pk)
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Create organisation
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Organisation.objects.get(name="Test Organisation")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_organisation"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_organisation"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Create venue
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_venue" and contains(@href, "add")]')
wait.until(animation_is_finished())
add_button.click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Venue.objects.get(name="Test Venue")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_venue"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_venue"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('3015-05-25')
form.find_element_by_id('id_start_time').send_keys('06:59')
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('4000-06-27')
form.find_element_by_id('id_end_time').send_keys('07:00')
# Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 1")
modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
e.send_keys(Keys.ENTER) # enter submit
# Confirm item has been saved to json field
objectitems = self.browser.execute_script("return objectitems;")
self.assertEqual(1, len(objectitems))
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
# See new item appear in table
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
self.assertIn("This is an item description", row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual(u'£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
self.assertEqual(u'£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# Attempt to save - missing title
save.click()
# See error
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
self.assertTrue(error.is_displayed())
# Should only have one error message
self.assertEqual("Name", error.find_element_by_xpath('//dt[1]').text)
self.assertEqual("This field is required.", error.find_element_by_xpath('//dd[1]/ul/li').text)
# don't need error so close it
error.find_element_by_xpath('//div[contains(@class, "alert-danger")]//button[@class="close"]').click()
try:
self.assertFalse(error.is_displayed())
except StaleElementReferenceException:
pass
except:
self.assertFail("Element does not appear to have been deleted")
# Check at least some data is preserved. Some = all will be there
option = self.browser.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
e.send_keys(Keys.ENTER)
# See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, successTitle)
except WebDriverException:
# This is a dirty workaround for wercker being a bit funny and not running it correctly.
# Waiting for wercker to get back to me about this
pass pass
except:
self.assertFail("Element does not appear to have been deleted")
# Check at least some data is preserved. Some = all will be there
option = self.browser.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
e.send_keys(Keys.ENTER)
# See redirected to success page
event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, self.browser.find_element_by_xpath('//h1').text)
def testEventDuplicate(self): def testEventDuplicate(self):
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end") testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end")
@@ -621,10 +608,9 @@ class EventTest(LiveServerTestCase):
save.click() save.click()
# See redirected to success page # See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name') event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, successTitle) self.assertIn("N0000%d | Test Event Name"%event.pk, self.browser.find_element_by_xpath('//h1').text)
def testRigNonRig(self): def testRigNonRig(self):
self.browser.get(self.live_server_url + '/event/create/') self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back # Gets redirected to login and back

View File

@@ -1,8 +1,6 @@
import pytz
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from RIGS import models from RIGS import models
from datetime import date, timedelta, datetime, time from datetime import date, timedelta
from decimal import * from decimal import *
@@ -203,72 +201,6 @@ class EventTestCase(TestCase):
event.status = models.Event.PROVISIONAL event.status = models.Event.PROVISIONAL
event.save() event.save()
def test_earliest_time(self):
event = models.Event(name="TE ET", start_date=date(2016, 01, 01))
# Just a start date
self.assertEqual(event.earliest_time, date(2016, 01, 01))
# With start time
event.start_time = time(9, 00)
self.assertEqual(event.earliest_time, self.create_datetime(2016, 1, 1, 9, 00))
# With access time
event.access_at = self.create_datetime(2015, 12, 03, 9, 57)
self.assertEqual(event.earliest_time, event.access_at)
# With meet time
event.meet_at = self.create_datetime(2015, 12, 03, 9, 55)
self.assertEqual(event.earliest_time, event.meet_at)
# Check order isn't important
event.start_date = date(2015, 12, 03)
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 03, 9, 00))
def test_latest_time(self):
event = models.Event(name="TE LT", start_date=date(2016, 01, 01))
# Just start date
self.assertEqual(event.latest_time, event.start_date)
# Just end date
event.end_date = date(2016, 1, 2)
self.assertEqual(event.latest_time, event.end_date)
# With end time
event.end_time = time(23, 00)
self.assertEqual(event.latest_time, self.create_datetime(2016, 1, 2, 23, 00))
def test_in_bounds(self):
manager = models.Event.objects
events = [
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'),
# basic checks
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'),
manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'),
manager.create(name='TE IB4', start_date='2016-01-04', access_at='2016-01-03'),
manager.create(name='TE IB5', start_date='2016-01-04', meet_at='2016-01-02'),
# negative check
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'),
]
in_bounds = manager.events_in_bounds(datetime(2016, 1, 2), datetime(2016, 1, 3))
self.assertIn(events[0], in_bounds)
self.assertIn(events[1], in_bounds)
self.assertIn(events[2], in_bounds)
self.assertIn(events[3], in_bounds)
self.assertIn(events[4], in_bounds)
self.assertIn(events[5], in_bounds)
self.assertNotIn(events[6], in_bounds)
def create_datetime(self, year, month, day, hour, min):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, min))
class EventItemTestCase(TestCase): class EventItemTestCase(TestCase):
def setUp(self): def setUp(self):

View File

@@ -1,157 +0,0 @@
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(self.venues[1].pk), unicode(self.venues[2].pk)],
'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 venue has been disposed of
self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[2].pk)
# Check the one we didn't delete is still there
self.assertEqual(models.Venue.objects.get(pk=self.venues[3].pk), self.venues[3])
# 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])
def test_person_merge(self):
change_url = reverse('admin:RIGS_person_changelist')
data = {'action': 'merge',
'_selected_action': [unicode(self.persons[1].pk), unicode(self.persons[2].pk)],
'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)
# Check the one we didn't delete is still there
self.assertEqual(models.Person.objects.get(pk=self.persons[3].pk), self.persons[3])
# 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])
def test_organisation_merge(self):
change_url = reverse('admin:RIGS_organisation_changelist')
data = {'action': 'merge',
'_selected_action': [unicode(self.organisations[1].pk), unicode(self.organisations[2].pk)],
'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)
# 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])

View File

@@ -69,8 +69,6 @@ urlpatterns = patterns('',
# Rigboard # Rigboard
url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'), url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'), url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')), url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')),
url(r'^rigboard/activity/$', url(r'^rigboard/activity/$',
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()), permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),

View File

@@ -1,18 +1,26 @@
import logging import logging
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.views import generic 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
# Versioning # Versioning
import reversion import reversion
import simplejson
from reversion.models import Version 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.db.models import IntegerField, EmailField, TextField from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from diff_match_patch import diff_match_patch from django.db.models import ForeignKey, IntegerField, EmailField, TextField
from RIGS import models from RIGS import models, forms
import datetime import datetime
import re
logger = logging.getLogger('tec.pyrigs') logger = logging.getLogger('tec.pyrigs')
@@ -20,10 +28,11 @@ logger = logging.getLogger('tec.pyrigs')
def model_compare(oldObj, newObj, excluded_keys=[]): def model_compare(oldObj, newObj, excluded_keys=[]):
# recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects # recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects
try: 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: except AttributeError:
theFields = newObj._meta.fields theFields = newObj._meta.fields
class FieldCompare(object): class FieldCompare(object):
def __init__(self, field=None, old=None, new=None): def __init__(self, field=None, old=None, new=None):
self.field = field self.field = field
@@ -41,13 +50,13 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
@property @property
def new(self): def new(self):
return self.display_value(self._new) return self.display_value(self._new)
@property @property
def long(self): def long(self):
if isinstance(self.field, EmailField): if isinstance(self.field, EmailField):
return True return True
return False return False
@property @property
def linebreaks(self): def linebreaks(self):
@@ -55,54 +64,27 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
return True return True
return False return False
@property
def diff(self):
oldText = unicode(self.display_value(self._old)) or ""
newText = unicode(self.display_value(self._new)) or ""
dmp = diff_match_patch()
diffs = dmp.diff_main(oldText, newText)
dmp.diff_cleanupSemantic(diffs)
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})
return outputDiffs
changes = [] changes = []
for thisField in theFields: for thisField in theFields:
name = thisField.name name = thisField.name
if name in excluded_keys: 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)
oldValue = getattr(oldObj, name, None) newValue = getattr(newObj, name, None)
except ObjectDoesNotExist:
oldValue = None
try:
newValue = getattr(newObj, name, None)
except ObjectDoesNotExist:
newValue = None
try: try:
bothBlank = (not oldValue) and (not newValue) bothBlank = (not oldValue) and (not newValue)
if oldValue != newValue and not bothBlank: if oldValue != newValue and not bothBlank:
compare = FieldCompare(thisField, oldValue, newValue) compare = FieldCompare(thisField,oldValue,newValue)
changes.append(compare) 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') logger.error('TypeError when comparing models')
return changes return changes
def compare_event_items(old, new): def compare_event_items(old, new):
# Recieves two event version objects and compares their items, returns an array of ItemCompare objects # Recieves two event version objects and compares their items, returns an array of ItemCompare objects
@@ -117,43 +99,39 @@ def compare_event_items(old, new):
self.changes = changes self.changes = changes
# Build some dicts of what we have # Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk 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 for version in old_item_versions: # put all the old versions in a list
if version.field_dict["event"] == old.object_id_int: compare = ItemCompare(old=version.object_version.object)
compare = ItemCompare(old=version.object_version.object) item_dict[version.object_id] = compare
item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions for version in new_item_versions: # go through the new versions
if version.field_dict["event"] == new.object_id_int: try:
try: compare = item_dict[version.object_id] # see if there's a matching old version
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
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
except KeyError: # there's no matching old version, so add this item to the dictionary by itself compare = ItemCompare(new=version.object_version.object)
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 = []
changes = []
for (_, compare) in item_dict.items(): 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: 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 return changes
def get_versions_for_model(models): def get_versions_for_model(models):
content_types = [] content_types = []
for model in models: for model in models:
content_types.append(ContentType.objects.get_for_model(model)) content_types.append(ContentType.objects.get_for_model(model))
versions = reversion.models.Version.objects.filter( versions = reversion.models.Version.objects.filter(
content_type__in=content_types, content_type__in = content_types,
).select_related("revision").order_by("-pk") ).select_related("revision").order_by("-pk")
return versions return versions
def get_previous_version(version): def get_previous_version(version):
thisId = version.object_id thisId = version.object_id
thisVersionId = version.pk thisVersionId = version.pk
@@ -161,19 +139,17 @@ def get_previous_version(version):
versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId) versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId)
try: try:
previousVersions = versions.filter(revision_id__lt=version.revision_id).latest( previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(field_name='revision__date_created')
field_name='revision__date_created')
except ObjectDoesNotExist: except ObjectDoesNotExist:
return False return False
return previousVersions return previousVersions
def get_changes_for_version(newVersion, oldVersion=None): def get_changes_for_version(newVersion, oldVersion=None):
# Pass in a previous version if you already know it (for efficiancy) #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 not provided then it will be looked up in the database
if oldVersion == None: if oldVersion == None:
oldVersion = get_previous_version(newVersion) oldVersion = get_previous_version(newVersion)
modelClass = newVersion.content_type.model_class() modelClass = newVersion.content_type.model_class()
@@ -197,7 +173,6 @@ def get_changes_for_version(newVersion, oldVersion=None):
return compare return compare
class VersionHistory(generic.ListView): class VersionHistory(generic.ListView):
model = reversion.revisions.Version model = reversion.revisions.Version
template_name = "RIGS/version_history.html" template_name = "RIGS/version_history.html"
@@ -213,7 +188,7 @@ class VersionHistory(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
thisModel = self.kwargs['model'] thisModel = self.kwargs['model']
context = super(VersionHistory, self).get_context_data(**kwargs) context = super(VersionHistory, self).get_context_data(**kwargs)
versions = context['object_list'] versions = context['object_list']
@@ -222,82 +197,81 @@ class VersionHistory(generic.ListView):
items = [] items = []
for versionNo, thisVersion in enumerate(versions): for versionNo, thisVersion in enumerate(versions):
if versionNo >= len(versions) - 1: if versionNo >= len(versions)-1:
thisItem = get_changes_for_version(thisVersion, None) thisItem = get_changes_for_version(thisVersion, None)
else: else:
thisItem = get_changes_for_version(thisVersion, versions[versionNo + 1]) thisItem = get_changes_for_version(thisVersion, versions[versionNo+1])
items.append(thisItem) items.append(thisItem)
context['object_list'] = items context['object_list'] = items
context['object'] = thisObject context['object'] = thisObject
return context return context
class ActivityTable(generic.ListView): class ActivityTable(generic.ListView):
model = reversion.revisions.Version model = reversion.revisions.Version
template_name = "RIGS/activity_table.html" template_name = "RIGS/activity_table.html"
paginate_by = 25 paginate_by = 25
def get_queryset(self): 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 return versions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Call the base implementation first to get a context # Call the base implementation first to get a context
context = super(ActivityTable, self).get_context_data(**kwargs) context = super(ActivityTable, self).get_context_data(**kwargs)
items = [] items = []
for thisVersion in context['object_list']: for thisVersion in context['object_list']:
thisItem = get_changes_for_version(thisVersion, None) thisItem = get_changes_for_version(thisVersion, None)
items.append(thisItem) items.append(thisItem)
context['object_list'] = items context ['object_list'] = items
return context return context
class ActivityFeed(generic.ListView): class ActivityFeed(generic.ListView):
model = reversion.revisions.Version model = reversion.revisions.Version
template_name = "RIGS/activity_feed_data.html" template_name = "RIGS/activity_feed_data.html"
paginate_by = 25 paginate_by = 25
def get_queryset(self): 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 return versions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
maxTimeDelta = [] maxTimeDelta = []
maxTimeDelta.append({'maxAge': datetime.timedelta(days=1), 'group': datetime.timedelta(hours=1)}) maxTimeDelta.append({ 'maxAge':datetime.timedelta(days=1), 'group':datetime.timedelta(hours=1)})
maxTimeDelta.append({'maxAge': None, 'group': datetime.timedelta(days=1)}) maxTimeDelta.append({ 'maxAge':None, 'group':datetime.timedelta(days=1)})
# Call the base implementation first to get a context # Call the base implementation first to get a context
context = super(ActivityFeed, self).get_context_data(**kwargs) context = super(ActivityFeed, self).get_context_data(**kwargs)
items = [] items = []
for thisVersion in context['object_list']: for thisVersion in context['object_list']:
thisItem = get_changes_for_version(thisVersion, None) thisItem = get_changes_for_version(thisVersion, None)
if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None: if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None:
thisItem['withPrevious'] = False thisItem['withPrevious'] = False
if len(items) >= 1: if len(items)>=1:
timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem[ timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem['revision'].date_created
'revision'].date_created
timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created
timeTogether = False timeTogether = False
for params in maxTimeDelta: for params in maxTimeDelta:
if params['maxAge'] is None or timeAgo <= params['maxAge']: if params['maxAge'] is None or timeAgo <= params['maxAge']:
timeTogether = timeDiff < params['group'] timeTogether = timeDiff < params['group']
break break
sameUser = thisItem['revision'].user == items[-1]['revision'].user sameUser = thisItem['revision'].user == items[-1]['revision'].user
thisItem['withPrevious'] = timeTogether & sameUser thisItem['withPrevious'] = timeTogether & sameUser
items.append(thisItem) items.append(thisItem)
context['object_list'] = items context ['object_list'] = items
return context return context

Binary file not shown.

View File

@@ -1,4 +1,3 @@
diff-match-patch==20121119
dj-database-url==0.3.0 dj-database-url==0.3.0
dj-static==0.0.6 dj-static==0.0.6
Django==1.8.2 Django==1.8.2
@@ -20,7 +19,7 @@ python-dateutil==2.4.2
pytz==2015.4 pytz==2015.4
raven==5.8.1 raven==5.8.1
reportlab==3.1.44 reportlab==3.1.44
selenium==2.53.1 selenium==2.46.0
simplejson==3.7.2 simplejson==3.7.2
six==1.9.0 six==1.9.0
sqlparse==0.1.15 sqlparse==0.1.15

View File

@@ -1,6 +0,0 @@
from django.contrib import admin
import models
# Register your models here.
admin.site.register(models.Hire)
admin.site.register(models.Provider)

View File

@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Hire',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('description', models.TextField(null=True, blank=True)),
('start_date', models.DateField()),
('end_date', models.DateField()),
('start_transport', models.IntegerField(blank=True, null=True, choices=[(0, b'TEC Transport'), (1, b'Provider Transports')])),
('mic', models.ForeignKey(related_name='hire_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
],
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=50)),
('phone', models.CharField(max_length=15, null=True, blank=True)),
('email', models.EmailField(max_length=254, null=True, blank=True)),
('address', models.TextField(null=True, blank=True)),
('notes', models.TextField(null=True, blank=True)),
],
),
migrations.AddField(
model_name='hire',
name='provider',
field=models.ForeignKey(blank=True, to='subhire.Provider', null=True),
),
]

View File

@@ -1,39 +0,0 @@
import reversion
from django.db import models
from django.conf import settings
from django.utils.encoding import python_2_unicode_compatible
# Create your models here.
class Hire(models.Model):
WE_TRANSPORT = 0
THEY_TRANSPORT = 1
TRANSPORT_CHOICES = (
(WE_TRANSPORT, 'TEC Transport'),
(THEY_TRANSPORT, 'Provider Transports'),
)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
provider = models.ForeignKey('Provider', blank=True, null=True)
start_date = models.DateField()
end_date = models.DateField()
start_transport = models.IntegerField(
choices=TRANSPORT_CHOICES, blank=True, null=True)
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='hire_mic', blank=True, null=True,
verbose_name="MIC")
class Provider(models.Model):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
@property
def latest_hires(self):
return self.hire_set.order_by('-start_date').select_related()

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -14,16 +14,14 @@
<link rel="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}"> <link rel="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}">
<link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}"> <link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet' <link href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet' type='text/css'>
type='text/css'>
<link rel="stylesheet" type="text/css" href="{% static "css/screen.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "css/screen.css" %}">
{% block css %} {% block css %}
{% endblock %} {% endblock %}
<script src="https://code.jquery.com/jquery-1.8.3.min.js" <script src="//code.jquery.com/jquery-latest.min.js"></script>
integrity="sha256-YcbK69I5IXQftf/mYD8WY0/KmEDCv1asggHpJk1trM8=" crossorigin="anonymous"></script>
<script src="https://cdn.ravenjs.com/1.3.0/jquery,native/raven.min.js"></script> <script src="https://cdn.ravenjs.com/1.3.0/jquery,native/raven.min.js"></script>
<script>Raven.config('{% sentry_public_dsn %}').install()</script> <script>Raven.config('{% sentry_public_dsn %}').install()</script>
{% block preload_js %} {% block preload_js %}
@@ -48,40 +46,32 @@
<div class="navbar-collapse"> <div class="navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Rigboard<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Rigboard<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> <li><a href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> Rigboard</a></li>
Rigboard</a></li> <li><a href="{% url 'event_archive' %}"><span class="glyphicon glyphicon-book"></span> Archive</a></li>
<li><a href="{% url 'event_archive' %}"><span class="glyphicon glyphicon-book"></span> <li><a href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> Calendar</a></li>
Archive</a></li> {% if perms.RIGS.view_event %}
<li><a href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> <li><a href="{% url 'activity_table' %}"><span class="glyphicon glyphicon-random"></span> Recent Changes</a></li>
Calendar</a></li> {% endif %}
{% if perms.RIGS.view_event %} {% if perms.RIGS.add_event %}
<li><a href="{% url 'activity_table' %}"><span <li><a href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> New Event</a></li>
class="glyphicon glyphicon-random"></span> Recent Changes</a></li> {% endif %}
{% endif %}
{% if perms.RIGS.add_event %} </ul>
<li><a href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> </li>
New Event</a></li>
{% endif %}
</ul>
</li>
{% endif %} {% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Active</a></li>
{% if perms.RIGS.add_invoice %} {% if perms.RIGS.add_invoice %}
<li><a href="{% url 'invoice_waiting' %}"><span <li><a href="{% url 'invoice_waiting' %}"><span class="glyphicon glyphicon-briefcase"></span> Waiting</a></li>
class="glyphicon glyphicon-briefcase"></span> Waiting</a></li>
{% endif %} {% endif %}
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Outstanding</a> <li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span> Archive</a></li>
</li>
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span>
Archive</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
@@ -94,7 +84,7 @@
{% if perms.RIGS.view_venue %} {% if perms.RIGS.view_venue %}
<li><a href="{% url 'venue_list' %}">Venues</a></li> <li><a href="{% url 'venue_list' %}">Venues</a></li>
{% endif %} {% endif %}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="dropdown"> <li class="dropdown">
@@ -154,6 +144,10 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
<div class="col-sm-12 text-center">
Reminder: Please consider carefully before booking rigs at this moment
</div>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
@@ -187,7 +181,7 @@
jQuery(document).on('click', '.modal-href', function (e) { jQuery(document).on('click', '.modal-href', function (e) {
$link = jQuery(this); $link = jQuery(this);
// Anti modal inception // Anti modal inception
if ($link.parents('#modal').length == 0) { if($link.parents('#modal').length == 0) {
e.preventDefault(); e.preventDefault();
modaltarget = $link.data('target'); modaltarget = $link.data('target');
modalobject = ""; modalobject = "";
@@ -199,11 +193,11 @@
var easter_egg = new Konami(); var easter_egg = new Konami();
easter_egg.code = function () { easter_egg.code = function() {
var s = document.createElement('script'); var s = document.createElement('script');
s.type = 'text/javascript'; s.type='text/javascript';
document.body.appendChild(s); document.body.appendChild(s);
s.src = '{% static "js/asteroids.min.js"%}'; s.src='{% static "js/asteroids.min.js"%}';
ga('send', 'event', 'easter_egg', 'activated'); ga('send', 'event', 'easter_egg', 'activated');
} }
easter_egg.load(); easter_egg.load();

View File

@@ -6,7 +6,7 @@
<form action="{% url 'login' %}" method="post" role="form">{% csrf_token %} <form action="{% url 'login' %}" method="post" role="form">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="id_username">{{ form.username.label }}</label> <label for="id_username">{{ form.username.label }}</label>
{% render_field form.username class+="form-control" placeholder=form.username.label autofocus="" %} {% render_field form.username class+="form-control" placeholder=form.username.label %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label> <label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label>

8
training/admin.py Normal file
View File

@@ -0,0 +1,8 @@
from django.contrib import admin
from training import models
# Register your models here.
admin.site.register(models.TrainingCategory)
admin.site.register(models.TrainingItem)
admin.site.register(models.TrainingRecord)
admin.site.register(models.TrainingLevelRecord)

762
training/exportSQL3.txt Normal file
View File

@@ -0,0 +1,762 @@
Option Compare Database
Option Explicit
' exportSQL version 3.2-dev
' www.rot13.org/~dpavlin/projects.html#sql
'
' based on exportSQL version 2.0 from www.cynergi.net/prod/exportsql/
'
' (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net
' (C) Pedro Freire - pedro.freire@cynergi.net (do not add to mailing lists without permission)
' (c) 2000-2001 Dobrica Pavlinusic <dpavlin@rot13.org> - added PostgreSQL support
'
' This code is provided free for anyone's use and is therefore without guarantee or support.
' This does NOT mean CYNERGI delegates its copyright to anyone using it! You may change the
' code in any way, as long as this notice remains on the code and CYNERGI is notified (if you
' publish the changes: if your changes/corrections prove valuable and are added to the code,
' you will be listed in a credit list on this file).
'
' You may NOT sell this as part of a non-free package:
' IF YOU HAVE PAID FOR THIS CODE, YOU HAVE BEEN ROBBED! CONTACT admin@cynergi.net!
' MODULE
' "exportSQL"
'
' GOAL
' Export all tables in a MS-Access database file to 2 text files:
' one containing SQL instructions to delete the new tables to be created,
' and the other with SQL instructions to create and insert data into
' the new tables. The table structure and data will resemble as much as
' possible the current Access database.
'
' HOW TO USE
' Copy-and-paste this text file into an Access module and run the first
' (and only public) function. in more detail, you:
' * Open the Access .mdb file you wish to export
' * in the default database objects window, click on "Modules", and then on "New"
' * The code window that opens has some pre-written text (code). Delete it.
' * Copy-and-paste this entire file to the code module window
' * If you are using Microsoft Access 2000 you will have to make
' one additional step: go into Tools/References and check following
' component: Microsoft DAO Object 3.6 Library and uncheck Microsoft
' ActiveX Data Objects Library
' * You may hit the compile button (looks like 3 sheets of paper with an arrow on
' top of them, pressing down on them), or select Debug, Compile Loaded Modules
' from the top menu, just to make sure there are no errors, and that this code
' works on your Access version (it works on Access'97 and should work on Access'95)
' * Close the code module window - windows will prompt you to save the code:
' answer "Yes", and when promped for a name for the module, type anything
' (say, "MexportSQL")
' The module is now part of your Access database. To run the export, you:
' * Re-open the code module (by double-clicking on it, or clicking "Design"
' with it selected). Move the cursor to where the first "Function" keyword appears.
' Press F5 or select Run, Go/Continue from the top menu.
' * Alternativelly, click on "Macros" on the database objects window,
' and then on "New". On the macro window, select "RunCode" as the macro action,
' and "exportSQL" as the function name, bellow. Save the macro similarly to the
' module, and this time double-clicking on it, or clicking "Run" will run the export.
'
' BEFORE RUNNING THE EXPORT
' Before running the export, be sure to check out the Export Options just bellow this
' text, and change any according to your wishes and specs.
'
' TECH DATA
' Public identifiers:
' * Only one: "exportSQL", a function taking and returning no arguments. It runs the export.
' Functionallity:
' * Can export to mSQL v1, mSQL v2, MySQL or PostgreSQL recognised SQL statements
' * Excellent respect for name conversion, namespace verification, type matching, etc.
' * Detects default values "=Now()", "=Date()" and "=Time()" to create types like "TIMESTAMP"
' * Fully configurable via private constants on top of code
' * Exports two files: one for erasures, another for creations (useful when updating dbs)
' * Generates compatibility warnings when necessary
' * Code and generated files are paragraphed and easy to read
' * Access text and memo fields can have any type of line termination: \n\r, \r\n, \n or \r
' * Properly escapes text and memo fields, besides all types of binary fields
' * Closes all open objects and files on error
' * Known bugs / incomplete constructs are signalled with comments starting with "!!!!"
' * Two alternatives on absent date/time type on mSQL: REAL or CHAR field
' * Exports Primary key and Indexes for PostgreSQL
' * Inserts Constrains as comments in SQL dump
' TODO:
' + fix fields with non-valid characters (-, /, and friend)
' + fix CR/LF in output
' + fix boolean fields
' + output of create table in separate file
' - create index (FIX)
' Export Options - change at will
Private Const DB_ENGINE As String = "Pg" ' USE ONLY "M1" (mSQL v1), "M2" (mSQL v2), "MY" (MySQL) or "Pg" (PostgreSQL)
Private Const DB_NAME As String = "" ' Use empty string for current. Else use filename or DSN name of database to export
Private Const DB_CONNECT As String = "" ' Used only if above string is not empty
Private Const MSQL_64kb_AVG As Long = 2048 ' ALWAYS < 65536 (to be consistent with MS Access). Set to max expected size of Access MEMO field (to preserve space in mSQL v1)
Private Const WS_REPLACEMENT As String = "_" ' Use "" to simply eat whitespaces in identifiers (table and field names)
Private Const IDENT_MAX_SIZE As Integer = 19 ' Suggest 64. Max size of identifiers (table and field names)
Private Const PREFIX_ON_KEYWORD As String = "_" ' Prefix to add to identifier, if it is a reserved word
Private Const SUFFIX_ON_KEYWORD As String = "" ' Suffix to add to identifier, if it is a reserved word
Private Const PREFIX_ON_INDEX As String = "ix" ' Prefix to add to index identifier, to make it unique (mSQL v2)
Private Const SUFFIX_ON_INDEX As String = "" ' Suffix to add to index identifier, to make it unique (mSQL v2)
Private Const CREATE_SQL_FILE As String = "c:\temp\esql_create.txt" ' Use empty if open on #1. Will be overwritten if exists!
Private Const DEL_SQL_FILE As String = "c:\temp\esql_del.txt" ' Use empty if open on #2. Will be overwritten if exists!
Private Const ADD_SQL_FILE As String = "c:\temp\esql_add.txt" ' Use empty if open on #1. Will be overwritten if exists!
Private Const LINE_BREAK As String = "\n" ' Try "<br>". String to replace line breaks in text fields
Private Const COMMENTS As Boolean = True ' Dump comments into output file
Private Const DISPLAY_WARNINGS As Boolean = True ' False to output the warnings to the files, only
Private Const DATE_AS_STR As Boolean = True ' False to use real number data type for date, time and timestamp (in mSQL only)
Private Const PARA_INSERT_AFTER As Integer = 3 ' Field count after which print INSERTs different lines
Private Const INDENT_SIZE As Integer = 5 ' Number of spaces on indents
' Global var to store inter-funtion data
Private warnings As String ' Not an option: do not set in any way
Private COMMENT_PREFIX As String
Private QUERY_SEPARATOR As String ' Terminator/separator of SQL queries (to instruct some monitor program to execute them)
' Primary Export Function
Sub exportSQL()
On Error GoTo exportSQL_error
Dim cdb As Database
Dim ctableix As Integer, ctablename As String
If COMMENTS Then
If DB_ENGINE = "Pg" Then
COMMENT_PREFIX = "--"
QUERY_SEPARATOR = ";"
Else
COMMENT_PREFIX = "#"
QUERY_SEPARATOR = "\g"
End If
End If
If DB_NAME = "" Then
Set cdb = CurrentDb()
Else
Set cdb = OpenDatabase(DB_NAME, False, True, DB_CONNECT) ' Shared, read-only
End If
If CREATE_SQL_FILE <> "" Then Open CREATE_SQL_FILE For Output As #1
If DEL_SQL_FILE <> "" Then Open DEL_SQL_FILE For Output As #2
If ADD_SQL_FILE <> "" Then Open ADD_SQL_FILE For Output As #3
DoCmd.Hourglass True
If COMMENTS Then
Dim convert_to As String
If (Left$(DB_ENGINE, 2) = "MY") Then
convert_to = "MySQL"
ElseIf (DB_ENGINE = "Pg") Then
convert_to = "PostgreSQL"
Else
convert_to = "mSQL"
End If
Print #1, COMMENT_PREFIX & " Exported from MS Access to " & convert_to
Print #2, COMMENT_PREFIX & " Exported from MS Access to " & convert_to
Print #3, COMMENT_PREFIX & " Exported from MS Access to " & convert_to
Print #1, COMMENT_PREFIX & " (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net"
Print #2, COMMENT_PREFIX & " (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net"
Print #3, COMMENT_PREFIX & " (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net"
End If
'Go through the table definitions
For ctableix = 0 To cdb.TableDefs.Count - 1
Dim cfieldix As Integer, cfieldname As String
Dim fieldlst As String, sqlcode As String
Dim primary_found As Boolean
Dim crs As Recordset
' Let's take only the visible tables
If (((cdb.TableDefs(ctableix).Attributes And DB_SYSTEMOBJECT) Or _
(cdb.TableDefs(ctableix).Attributes And DB_HIDDENOBJECT))) = 0 Then
ctablename = conv_name("" & cdb.TableDefs(ctableix).Name)
Print #2,
Print #2, "DROP TABLE " & ctablename & QUERY_SEPARATOR
' CREATE clause
Print #1,
Print #1, "CREATE TABLE " & ctablename
Print #1, Space$(INDENT_SIZE) & "("
warnings = ""
fieldlst = ""
primary_found = False
' loop thorugh each field in the table
For cfieldix = 0 To cdb.TableDefs(ctableix).Fields.Count - 1
Dim typestr As String, fieldsz As Integer, dvstr As String
Dim found_ix As Boolean, cindex, tmpindex As Index, cfield, tmpfield As Field
' if this is not the first iteration, add separators
If fieldlst <> "" Then
fieldlst = fieldlst & ", "
Print #1, ","
End If
' get field name
cfieldname = conv_name("" & cdb.TableDefs(ctableix).Fields(cfieldix).Name)
fieldlst = fieldlst & cfieldname
' translate types
If DB_ENGINE = "M1" Or DB_ENGINE = "M2" Then
Select Case cdb.TableDefs(ctableix).Fields(cfieldix).Type
Case dbChar
typestr = "CHAR(" & cdb.TableDefs(ctableix).Fields(cfieldix).Size & ")"
Case dbText
fieldsz = cdb.TableDefs(ctableix).Fields(cfieldix).Size
If fieldsz = 0 Then fieldsz = 255
typestr = "CHAR(" & fieldsz & ")"
Case dbBoolean, dbByte, dbInteger, dbLong
typestr = "INT"
Case dbDouble, dbFloat, dbSingle
typestr = "REAL"
Case dbCurrency, dbDecimal, dbNumeric
typestr = "REAL"
warn "In new field '" & cfieldname & "', currency/BCD will be converted to REAL - there may be precision loss!", False
Case dbDate
typestr = IIf(DATE_AS_STR, "CHAR(19)", "REAL") ' use Access internal format: IEEE 64-bit (8-byte) FP
warn "In new field '" & cfieldname & "', date/time/timestamp will be converted to " & typestr & ".", False
Case dbTime
typestr = IIf(DATE_AS_STR, "CHAR(8)", "REAL") ' use Access internal format: IEEE 64-bit (8-byte) FP
warn "In new field '" & cfieldname & "', date/time/timestamp will be converted to " & typestr & ".", False
Case dbTimeStamp
typestr = IIf(DATE_AS_STR, "CHAR(19)", "REAL") ' use Access internal format: IEEE 64-bit (8-byte) FP
warn "In new field '" & cfieldname & "', date/time/timestamp will be converted to " & typestr & "." & IIf(DB_ENGINE = "M2", " Consider using pseudo field '_timestamp'.", ""), False
Case dbMemo
If DB_ENGINE = "M2" Then
typestr = "TEXT(" & MSQL_64kb_AVG & ")"
Else
typestr = "CHAR(" & MSQL_64kb_AVG & ")"
warn "In new field '" & cfieldname & "', dbMemo is not supported by mSQL v1 - fields larger than MSQL_64kb_AVG (" & MSQL_64kb_AVG & ") will not be accepted!", False
End If
Case dbBinary, dbVarBinary
typestr = "CHAR(255)"
warn "In new field '" & cfieldname & "', dbBinary and dbVarBinary are not supported by mSQL! - will use a text (CHAR(255)) field.", True
Case dbLongBinary
typestr = "CHAR(" & MSQL_64kb_AVG & ")"
warn "In new field '" & cfieldname & "', dbLongBinary is not supported by mSQL! - will use a text (CHAR(" & MSQL_64kb_AVG & ")) field.", True
Case Else
warn "In new field '" & cfieldname & "', dbBigInt and dbGUID are not currently supported!", True
Error 5 ' invalid Procedure Call
End Select
ElseIf DB_ENGINE = "MY" Then
Select Case cdb.TableDefs(ctableix).Fields(cfieldix).Type
Case dbBinary
typestr = "TINYBLOB"
Case dbBoolean
typestr = "TINYINT"
Case dbByte
typestr = "TINYINT UNSIGNED"
Case dbChar
typestr = "CHAR(" & cdb.TableDefs(ctableix).Fields(cfieldix).Size & ")"
Case dbCurrency
typestr = "DECIMAL(20,4)"
Case dbDate
typestr = "DATETIME"
Case dbDecimal
typestr = "DECIMAL(20,4)"
Case dbDouble
typestr = "REAL"
Case dbFloat
typestr = "REAL"
Case dbInteger
typestr = "SMALLINT"
Case dbLong
typestr = "INT"
Case dbLongBinary
typestr = "LONGBLOB"
Case dbMemo
typestr = "LONGBLOB" ' !!!!! MySQL bug! Replace by LONGTEXT when corrected!
Case dbNumeric
typestr = "DECIMAL(20,4)"
Case dbSingle
typestr = "FLOAT"
Case dbText
fieldsz = cdb.TableDefs(ctableix).Fields(cfieldix).Size
If fieldsz = 0 Then fieldsz = 255
typestr = "CHAR(" & fieldsz & ")"
Case dbTime
typestr = "TIME"
Case dbTimeStamp
typestr = "TIMESTAMP"
Case dbVarBinary
typestr = "TINYBLOB"
Case dbBigInt, dbGUID
warn "In new field '" & cfieldname & "', dbBigInt and dbGUID are not currently supported!", True
Error 5 ' invalid Procedure Call
Case Else
typestr = "LONGBLOB"
End Select
ElseIf DB_ENGINE = "Pg" Then
Select Case cdb.TableDefs(ctableix).Fields(cfieldix).Type
Case dbBinary
typestr = "int2"
Case dbBoolean
typestr = "bool"
Case dbByte
typestr = "int2"
Case dbChar
typestr = "varchar(" & cdb.TableDefs(ctableix).Fields(cfieldix).Size & ")"
Case dbCurrency
typestr = "DECIMAL(20,4)"
Case dbDate
typestr = "DATETIME"
Case dbDecimal
typestr = "DECIMAL(20,4)"
Case dbDouble
typestr = "float8"
Case dbFloat
typestr = "float4"
Case dbInteger
typestr = "int4"
Case dbLong
typestr = "int8"
Case dbLongBinary
typestr = "text" ' hm?
Case dbMemo
typestr = "text"
Case dbNumeric
typestr = "DECIMAL(20,4)"
Case dbSingle
typestr = "float4"
Case dbText
fieldsz = cdb.TableDefs(ctableix).Fields(cfieldix).Size
If fieldsz = 0 Then fieldsz = 255
typestr = "varchar(" & fieldsz & ")"
Case dbTime
typestr = "TIME"
Case dbTimeStamp
typestr = "TIMESTAMP"
Case dbVarBinary
typestr = "text" ' hm?
Case dbBigInt, dbGUID
warn "In new field '" & cfieldname & "', dbBigInt and dbGUID are not currently supported!", True
Error 5 ' invalid Procedure Call
Case Else
typestr = "text"
End Select
Else
warn "unkown DB_ENGINE string " & DB_ENGINE, True
Error 5 ' invalid Procedure Call
End If
' check not null and auto-increment properties
If ((cdb.TableDefs(ctableix).Fields(cfieldix).Attributes And dbAutoIncrField) <> 0) Then
If Left$(DB_ENGINE, 2) = "MY" Then
typestr = typestr & " NOT NULL AUTO_INCREMENT"
ElseIf DB_ENGINE = "Pg" Then
typestr = " serial"
Else
typestr = typestr & " NOT NULL"
warn "In new field '" & cfieldname & "', mSQL does not support auto-increment fields! - they will be pure INTs." & IIf(DB_ENGINE = "M2", " Consider using pseudo field '_rowid' or SEQUENCEs.", ""), False
End If
ElseIf cdb.TableDefs(ctableix).Fields(cfieldix).Required = True Then
typestr = typestr & " NOT NULL"
End If
' default value
dvstr = cdb.TableDefs(ctableix).Fields(cfieldix).DefaultValue
If dvstr <> "" Then
If Left$(DB_ENGINE, 2) <> "MY" And DB_ENGINE <> "Pg" Then
warn "In new field '" & cfieldname & "', mSQL does not support default values! - they won't be initialised.", False
ElseIf Left$(DB_ENGINE, 2) = "MY" And cdb.TableDefs(ctableix).Fields(cfieldix).Required = False Then
warn "In new field '" & cfieldname & "', MySQL needs NOT NULL to support default values! - it won't be set a default.", False
ElseIf Left$(dvstr, 1) = """" Then
typestr = typestr & " DEFAULT '" & conv_str(Mid$(dvstr, 2, Len(dvstr) - 2)) & "'"
ElseIf ((LCase(dvstr) = "now()" Or LCase(dvstr) = "date()" Or LCase(dvstr) = "time()") And _
(Left$(typestr, 5) = "DATE " Or Left$(typestr, 5) = "TIME " Or Left$(typestr, 9) = "DATETIME ")) Then
typestr = "TIMESTAMP " & Right$(typestr, Len(typestr) - InStr(typestr, " "))
ElseIf LCase(dvstr) = "no" Then
typestr = typestr & " DEFAULT 0"
ElseIf LCase(dvstr) = "yes" Then
typestr = typestr & " DEFAULT 1"
Else
typestr = typestr & " DEFAULT " & dvstr
End If
End If
' add constrains
Dim val_rule, val_text As String
val_rule = cdb.TableDefs(ctableix).Fields(cfieldix).ValidationRule
val_text = cdb.TableDefs(ctableix).Fields(cfieldix).ValidationText
If DB_ENGINE = "Pg" And val_rule <> "" Then
typestr = typestr & COMMENT_PREFIX & " check ( " & val_rule & " ) " & COMMENT_PREFIX & " " & val_text
warn "Field '" & cfieldname & "' has constrain '" & val_rule & "' with text '" & val_text & "' which you have to convert manually (inserted as comment in SQL)", False
End If
' check if primary key (for mSQL v1)
If DB_ENGINE = "M1" Then
found_ix = False
For Each cindex In cdb.TableDefs(ctableix).Indexes
If cindex.Primary Then
For Each cfield In cindex.Fields
If cfield.Name = cdb.TableDefs(ctableix).Fields(cfieldix).Name Then
found_ix = True
Exit For
End If
Next cfield
If found_ix Then Exit For
End If
Next cindex
If found_ix Then
If primary_found Then
warn "On new table '" & ctablename & "', mSQL v1 does not support more than one PRIMARY KEY! Only first key was set.", False
Else
typestr = typestr & " PRIMARY KEY"
primary_found = True
End If
End If
End If
'print out field info
Print #1, Space$(INDENT_SIZE) & cfieldname & Space$(IDENT_MAX_SIZE - Len(cfieldname) + 2) & typestr;
Next cfieldix
' terminate CREATE clause
If DB_ENGINE = "M2" Then
Print #1,
Print #1, Space$(INDENT_SIZE) & ")" & QUERY_SEPARATOR
End If
' primary key and other index declaration
If DB_ENGINE = "M2" Or Left$(DB_ENGINE, 2) = "MY" Or DB_ENGINE = "Pg" Then
For Each cindex In cdb.TableDefs(ctableix).Indexes
sqlcode = ""
For Each cfield In cindex.Fields
sqlcode = sqlcode & IIf(sqlcode = "", "", ", ") & conv_name(cfield.Name)
Next cfield
If DB_ENGINE = "M2" Then
Print #1, "CREATE " & IIf(cindex.Unique, "UNIQUE ", "") & "INDEX " & _
conv_name(PREFIX_ON_INDEX & cindex.Name & SUFFIX_ON_INDEX) & " ON " & _
ctablename & " (" & sqlcode & ")" & QUERY_SEPARATOR
ElseIf DB_ENGINE = "Pg" Then
If cindex.Primary Then
Print #1, ","
Print #1, Space$(INDENT_SIZE) & "PRIMARY KEY (" & sqlcode & ")";
ElseIf cindex.Unique Then
Print #1, ","
Print #1, Space$(INDENT_SIZE) & "UNIQUE INDEX (" & sqlcode & ")";
Else
' skip indexes which are part of primary key
primary_found = False
For Each tmpindex In cdb.TableDefs(ctableix).Indexes
If tmpindex.Primary Then
For Each tmpfield In tmpindex.Fields
If sqlcode = conv_name(tmpfield.Name) Then
primary_found = True
Exit For
End If
Next tmpfield
End If
Next tmpindex
If Not primary_found Then
If DB_ENGINE = "Pg" Then
' FIX: create index....
Else
Print #1, ","
Print #1, Space$(INDENT_SIZE) & "INDEX (" & sqlcode & ")";
End If
End If
End If
Else
Print #1, ","
Print #1, Space$(INDENT_SIZE) & IIf(cindex.Primary, "PRIMARY ", "") & "KEY (" & sqlcode & ")";
End If
Next cindex
End If
' terminate CREATE clause
If DB_ENGINE <> "M2" Then
Print #1,
Print #1, Space$(INDENT_SIZE) & ")" & QUERY_SEPARATOR
End If
' print any warnings bellow it
If COMMENTS And warnings <> "" Then
If DB_ENGINE = "M2" Then Print #1, COMMENT_PREFIX & " "
Print #1, warnings
warnings = ""
End If
Print #1,
' INSERT clause
Set crs = cdb.OpenRecordset(cdb.TableDefs(ctableix).Name)
If crs.RecordCount <> 0 Then
' loop thorugh each record in the table
crs.MoveFirst
Do Until crs.EOF
' start paragraphing
sqlcode = "INSERT INTO " & ctablename
If crs.Fields.Count > PARA_INSERT_AFTER Then
Print #3, sqlcode
If DB_ENGINE = "M1" Then Print #3, Space$(INDENT_SIZE) & "(" & fieldlst & ")"
Print #3, "VALUES ("
sqlcode = Space$(INDENT_SIZE)
Else
If DB_ENGINE = "M1" Then sqlcode = sqlcode & " (" & fieldlst & ")"
sqlcode = sqlcode & " VALUES ("
End If
' loop through each field in each record
For cfieldix = 0 To crs.Fields.Count - 1
' based on type, prepare the field value
If IsNull(crs.Fields(cfieldix).Value) Then
sqlcode = sqlcode & "NULL"
Else
Select Case crs.Fields(cfieldix).Type
Case dbBoolean
If DB_ENGINE = "Pg" Then
sqlcode = sqlcode & IIf(crs.Fields(cfieldix).Value = True, "'t'", "'f'")
Else
sqlcode = sqlcode & IIf(crs.Fields(cfieldix).Value = True, "1", "0")
End If
Case dbChar, dbText, dbMemo
sqlcode = sqlcode & "'" & conv_str(crs.Fields(cfieldix).Value) & "'"
Case dbDate, dbTimeStamp
If Left$(DB_ENGINE, 2) = "MY" Or DATE_AS_STR Then
sqlcode = sqlcode & "'" & Format(crs.Fields(cfieldix).Value, "YYYY-MM-DD HH:MM:SS") & "'"
Else
'print in Access internal format: IEEE 64-bit (8-byte) FP
sqlcode = sqlcode & "'" & Format(crs.Fields(cfieldix).Value, "#.#########") & "'"
End If
Case dbTime
If Left$(DB_ENGINE, 2) = "MY" Or DATE_AS_STR Then
sqlcode = sqlcode & "'" & Format(crs.Fields(cfieldix).Value, "HH:MM:SS") & "'"
Else
'print in Access internal format: IEEE 64-bit (8-byte) FP
sqlcode = sqlcode & "'" & Format(crs.Fields(cfieldix).Value, "#.#########") & "'"
End If
Case dbBinary, dbLongBinary, dbVarBinary
sqlcode = sqlcode & "'" & conv_bin(crs.Fields(cfieldix).Value) & "'"
Case dbCurrency, dbDecimal, dbDouble, dbFloat, dbNumeric, dbSingle
sqlcode = sqlcode & conv_float(crs.Fields(cfieldix).Value)
Case Else
sqlcode = sqlcode & conv_str(crs.Fields(cfieldix).Value)
End Select
End If
' paragraph separators
If cfieldix < crs.Fields.Count - 1 Then
sqlcode = sqlcode & ", "
If crs.Fields.Count > PARA_INSERT_AFTER Then
Print #3, sqlcode
sqlcode = Space$(INDENT_SIZE)
End If
End If
Next cfieldix
' print out result and any warnings
sqlcode = sqlcode & IIf(crs.Fields.Count > PARA_INSERT_AFTER, " )", ")") & QUERY_SEPARATOR
Print #3, sqlcode
If COMMENTS And warnings <> "" Then
Print #3, warnings
warnings = ""
End If
If crs.Fields.Count > PARA_INSERT_AFTER Then Print #3,
crs.MoveNext
Loop
Else
' if there is no data on the table
If COMMENTS Then Print #3, COMMENT_PREFIX & " This table has no data"
End If
crs.Close
Set crs = Nothing
End If 'print only unhidden tables
Next ctableix
exportSQL_exit:
Close #3
Close #2
Close #1
cdb.Close
Set cdb = Nothing
DoCmd.Hourglass False
Exit Sub
exportSQL_error:
MsgBox Err.Description
Resume exportSQL_exit
End Sub
Private Function conv_name(strname As String) As String
Dim i As Integer, str As String
' replace inner spaces with WS_REPLACEMENT
str = strname
i = 1
While i <= Len(str)
Select Case Mid$(str, i, 1)
Case " ", Chr$(9), Chr$(10), Chr$(13), "-", "/" ' space, tab, newline, carriage return
str = Left$(str, i - 1) & WS_REPLACEMENT & Right$(str, Len(str) - i)
i = i + Len(WS_REPLACEMENT)
Case Else
i = i + 1
End Select
Wend
' restrict tablename to IDENT_MAX_SIZE chars, *after* eating spaces
str = Left$(str, IDENT_MAX_SIZE)
' check for reserved words
conv_name = str
If Left$(DB_ENGINE, 2) = "MY" Then
Select Case LCase$(str)
Case "add", "all", "alter", "and", "as", "asc", "auto_increment", "between", _
"bigint", "binary", "blob", "both", "by", "cascade", "char", "character", _
"change", "check", "column", "columns", "create", "data", "datetime", "dec", _
"decimal", "default", "delete", "desc", "describe", "distinct", "double", _
"drop", "escaped", "enclosed", "explain", "fields", "float", "float4", _
"float8", "foreign", "from", "for", "full", "grant", "group", "having", _
"ignore", "in", "index", "infile", "insert", "int", "integer", "interval", _
"int1", "int2", "int3", "int4", "int8", "into", "is", "key", "keys", _
"leading", "like", "lines", "limit", "lock", "load", "long", "longblob", _
"longtext", "match", "mediumblob", "mediumtext", "mediumint", "middleint", _
"numeric", "not", "null", "on", "option", "optionally", "or", "order", _
"outfile", "partial", "precision", "primary", "procedure", "privileges", _
"read", "real", "references", "regexp", "repeat", "replace", "restrict", _
"rlike", "select", "set", "show", "smallint", "sql_big_tables", _
"sql_big_selects", "sql_select_limit", "straight_join", "table", "tables", _
"terminated", "tinyblob", "tinytext", "tinyint", "trailing", "to", "unique", _
"unlock", "unsigned", "update", "usage", "values", "varchar", "varying", _
"with", "write", "where", "zerofill"
conv_name = Left$(PREFIX_ON_KEYWORD & str & SUFFIX_ON_KEYWORD, IDENT_MAX_SIZE)
If (str = conv_name) Then
warn "In identifier '" & strname & "', the new form '" & strname & _
"' is a reserved word, and PREFIX_ON_KEYWORD ('" & _
PREFIX_ON_KEYWORD & "') and SUFFIX_ON_KEYWORD ('" & SUFFIX_ON_KEYWORD & _
"') make it larger than IDENT_MAX_SIZE, and after cut it is the same as the original! " & _
"This is usually caused by a void or empty PREFIX_ON_KEYWORD.", True
Error 5 ' invalid Procedure Call
End If
End Select
End If
End Function
Private Function conv_str(str As String) As String
Dim i As Integer, nlstr As String, rstr As Variant
nlstr = ""
rstr = Null
i = 1
While i <= Len(str)
Select Case Mid$(str, i, 1)
Case Chr$(0) ' ASCII NUL
nlstr = ""
rstr = "\0"
Case Chr$(8) ' backspace
nlstr = ""
rstr = "\b"
Case Chr$(9) ' tab
nlstr = ""
rstr = "\t"
Case "'"
nlstr = ""
rstr = "\'"
Case """"
nlstr = ""
rstr = "\"""
Case "\"
nlstr = ""
rstr = "\\"
Case Chr$(10), Chr$(13) ' line feed and carriage return
If nlstr <> "" And nlstr <> Mid$(str, i, 1) Then
' there was a previous newline and this is its pair: eat it
rstr = ""
nlstr = ""
Else
' this is a fresh newline
rstr = LINE_BREAK
nlstr = Mid$(str, i, 1)
End If
Case Else
nlstr = ""
End Select
If Not IsNull(rstr) Then
str = Left$(str, i - 1) & rstr & Right$(str, Len(str) - i)
i = i + Len(rstr)
rstr = Null
Else
i = i + 1
End If
Wend
conv_str = str
End Function
Private Function conv_bin(str As String) As String
Dim i As Integer, rstr As String
rstr = ""
i = 1
While i <= Len(str)
Select Case Mid$(str, i, 1)
Case Chr$(0) ' ASCII NUL
rstr = "\0"
Case Chr$(8) ' backspace
rstr = "\b"
Case Chr$(9) ' tab
rstr = "\t"
Case "'"
rstr = "\'"
Case """"
rstr = "\"""
Case "\"
rstr = "\\"
Case Chr$(10) ' line feed
rstr = "\n"
Case Chr$(13) ' carriage return
rstr = "\r"
End Select
If rstr <> "" Then
str = Left$(str, i - 1) & rstr & Right$(str, Len(str) - i)
i = i + Len(rstr)
rstr = ""
Else
i = i + 1
End If
Wend
conv_bin = str
End Function
' This function is used to convert local setting of decimal , to .
Private Function conv_float(str As String) As String
Dim i As Integer
i = 1
While i <= Len(str)
If Mid$(str, i, 1) = "," Then
str = Left$(str, i - 1) & "." & Right$(str, Len(str) - i)
End If
i = i + 1
Wend
conv_float = str
End Function
Private Sub warn(str As String, abortq As Boolean)
If DISPLAY_WARNINGS Then MsgBox str, vbOKOnly Or vbExclamation, "Warning"
warnings = warnings & COMMENT_PREFIX & " Warning: " & str & Chr$(13) & Chr$(10)
End Sub

View File

@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TrainingCategory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('category_number', models.PositiveSmallIntegerField()),
('category_name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='TrainingItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('item_number', models.PositiveSmallIntegerField()),
('item_name', models.CharField(max_length=100)),
('category', models.ForeignKey(to='training.TrainingCategory')),
],
),
migrations.CreateModel(
name='TrainingLevelRecord',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('technical_assistant', models.DateField(null=True, blank=True)),
('sound_technician', models.DateField(null=True, blank=True)),
('sound_supervisor', models.DateField(null=True, blank=True)),
('lighting_technician', models.DateField(null=True, blank=True)),
('lighting_supervisor', models.DateField(null=True, blank=True)),
('power_technician', models.DateField(null=True, blank=True)),
('power_supervisor', models.DateField(null=True, blank=True)),
('haulage_supervisor', models.DateField(null=True, blank=True)),
('trainee', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='TrainingRecord',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('started_date', models.DateField(null=True, blank=True)),
('started_notes', models.TextField(null=True, blank=True)),
('completed_date', models.DateField(null=True, blank=True)),
('completed_notes', models.TextField(null=True, blank=True)),
('assessed_date', models.DateField(null=True, blank=True)),
('assessed_notes', models.TextField(null=True, blank=True)),
('assessed_trainer', models.ForeignKey(related_name='trainingrecords_assessed', to=settings.AUTH_USER_MODEL)),
('completed_trainer', models.ForeignKey(related_name='trainingrecords_completed', to=settings.AUTH_USER_MODEL)),
('started_trainer', models.ForeignKey(related_name='trainingrecords_started', to=settings.AUTH_USER_MODEL)),
('trainee', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('training_item', models.ForeignKey(to='training.TrainingItem')),
],
),
migrations.AddField(
model_name='trainingitem',
name='training_records',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='training.TrainingRecord'),
),
migrations.AlterUniqueTogether(
name='trainingrecord',
unique_together=set([('trainee', 'training_item')]),
),
]

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('training', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='trainingrecord',
name='assessed_trainer',
field=models.ForeignKey(related_name='trainingrecords_assessed', blank=True, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AlterField(
model_name='trainingrecord',
name='completed_trainer',
field=models.ForeignKey(related_name='trainingrecords_completed', blank=True, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AlterField(
model_name='trainingrecord',
name='started_trainer',
field=models.ForeignKey(related_name='trainingrecords_started', blank=True, to=settings.AUTH_USER_MODEL, null=True),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
-- Exported from MS Access to PostgreSQL
-- (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net
CREATE TABLE Categories
(
ID serial,
Category_Number int8,
Category_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE Members
(
ID serial,
Member_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE tblrequirements
(
ID serial,
level_id int8,
item_id int8,
PRIMARY KEY (ID)
);
CREATE TABLE Training_Items
(
ID serial,
Category_ID int8,
Item_Number int8,
Item_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE Training_Level_Reco
(
ID serial,
Member_ID int8,
Training_Level_ID int8,
Date_Level_Awarded DATETIME,
PRIMARY KEY (ID)
);
CREATE TABLE Training_Levels
(
ID serial,
Level_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE Training_Records
(
ID serial,
Member_ID int8,
Training_Item_ID int8,
Traning_Started_Dat DATETIME,
Training_Started_As int8,
Training_Started_No text,
Traning_Complete_Da DATETIME,
Training_Complete_A int8,
Training_Complete_N text,
Competency_Assessed DATETIME,
Competency_Assessed int8,
Competency_Assessed text,
PRIMARY KEY (ID)
);

View File

@@ -0,0 +1,16 @@
-- Exported from MS Access to PostgreSQL
-- (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net
DROP TABLE Categories;
DROP TABLE Members;
DROP TABLE tblrequirements;
DROP TABLE Training_Items;
DROP TABLE Training_Level_Reco;
DROP TABLE Training_Levels;
DROP TABLE Training_Records;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
-- Exported from MS Access to PostgreSQL
-- (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net
CREATE TABLE Categories
(
ID serial,
Category_Number int8,
Category_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE Members
(
ID serial,
Member_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE tblrequirements
(
ID serial,
level_id int8,
item_id int8,
PRIMARY KEY (ID)
);
CREATE TABLE Training_Items
(
ID serial,
Category_ID int8 REFERENCES Categories,
Item_Number int8,
Item_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE Training_Level_Records
(
ID serial,
Member_ID int8,
Training_Level_ID int8,
Date_Level_Awarded DATE,
PRIMARY KEY (ID)
);
CREATE TABLE Training_Levels
(
ID serial,
Level_Name varchar(255),
PRIMARY KEY (ID)
);
CREATE TABLE Training_Records
(
ID serial,
Member_ID int8 REFERENCES Members,
Training_Item_ID int8 REFERENCES Training_Items,
Started_Date DATE,
Started_Assessor int8 REFERENCES Members,
Started_Notes text,
Complete_Date DATE,
Complete_Assessor int8 REFERENCES Members,
Complete_Notes text,
Assessed_Date DATE,
Assessed_Assessor int8 REFERENCES Members,
Assessed_Notes text,
PRIMARY KEY (ID)
);

View File

@@ -0,0 +1,16 @@
-- Exported from MS Access to PostgreSQL
-- (C) 1997-98 CYNERGI - www.cynergi.net, info@cynergi.net
DROP TABLE Categories;
DROP TABLE Members;
DROP TABLE tblrequirements;
DROP TABLE Training_Items;
DROP TABLE Training_Level_Records;
DROP TABLE Training_Levels;
DROP TABLE Training_Records;

90
training/models.py Normal file
View File

@@ -0,0 +1,90 @@
from django.db import models
from django.conf import settings
from django.utils.encoding import python_2_unicode_compatible
import reversion
# Create your models here.
@python_2_unicode_compatible
@reversion.register
class TrainingCategory(models.Model):
category_number = models.PositiveSmallIntegerField()
category_name = models.CharField(max_length=50)
def __str__(self):
return "{0}: {1}".format(self.category_number, self.category_name)
@python_2_unicode_compatible
@reversion.register
class TrainingItem(models.Model):
category = models.ForeignKey(TrainingCategory)
item_number = models.PositiveSmallIntegerField()
item_name = models.CharField(max_length=100)
training_records = models.ManyToManyField(settings.AUTH_USER_MODEL,
through='TrainingRecord', through_fields=('training_item', 'trainee'))
def __str__(self):
return "{0}.{1}: {2}".format(self.category.category_number, self.item_number, self.item_name)
@python_2_unicode_compatible
@reversion.register
class TrainingRecord(models.Model):
trainee = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='trainingrecords')
training_item = models.ForeignKey(TrainingItem)
started_date = models.DateField(blank=True, null=True)
started_trainer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='trainingrecords_started', blank=True,
null=True)
started_notes = models.TextField(blank=True, null=True)
completed_date = models.DateField(blank=True, null=True)
completed_trainer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='trainingrecords_completed',
blank=True, null=True)
completed_notes = models.TextField(blank=True, null=True)
assessed_date = models.DateField(blank=True, null=True)
assessed_trainer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='trainingrecords_assessed', blank=True,
null=True)
assessed_notes = models.TextField(blank=True, null=True)
@property
def started(self):
return self.started_date and self.started_trainer
@property
def complete(self):
return self.completed_date and self.completed_trainer
@property
def assessed(self):
return self.assessed_date and self.assessed_trainer
def __str__(self):
return "{0} - {1}".format(self.trainee, self.training_item)
class Meta:
unique_together = ('trainee', 'training_item')
@python_2_unicode_compatible
@reversion.register
class TrainingLevelRecord(models.Model):
trainee = models.ForeignKey(settings.AUTH_USER_MODEL)
technical_assistant = models.DateField(blank=True, null=True)
sound_technician = models.DateField(blank=True, null=True)
sound_supervisor = models.DateField(blank=True, null=True)
lighting_technician = models.DateField(blank=True, null=True)
lighting_supervisor = models.DateField(blank=True, null=True)
power_technician = models.DateField(blank=True, null=True)
power_supervisor = models.DateField(blank=True, null=True)
haulage_supervisor = models.DateField(blank=True, null=True)
def __str__(self):
return "{0}".format(self.trainee)

View File

@@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block title %}Training Database{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12 col-md-4">
<h2>Training Database</h2>
</div>
<div class="col-sm-12 col-md-8 align-right">
{# @todo: Add nav buttons and other useful things in here #}
</div>
</div>
<h3>My Training</h3>
{% include "training/profile_detail_table.html" %}
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block title %}{{ profile }} Training{% endblock %}
{% block content %}
<h2>Training Profile for {{ profile }}</h2>
{% include "training/profile_detail_table.html" %}
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% for category in categories %}
<div class="row">
<div class="col-xs-12">
{% include "training/trainingcategory_item_table.html" %}
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,52 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% block title %}Training Items{% endblock %}
{% block content %}
<h2>Training Items</h2>
<div class="row toc-wrapper">
<div class="col-xs-12 hidden-md hidden-lg">
<ul class="nav nav-pills">
{% for category in object_list %}
<li role="presentation"><a href="#{{ category.category_name|slugify }}">{{ category }}</a></li>
{% endfor %}
</ul>
</div>
<div class="col-xs-12 col-lg-9" id="fish">
{% for category in object_list %}
<div style="margin-bottom: 50em;">
<a class="anchor" id="{{ category.category_name|slugify }}"></a>
<h3>{{ category }}</h3>
&nbsp;
</div>
{% endfor %}
</div>
<div class="col-md-3 visible-md visible-lg">
<div class="toc-nav">
<h3>Available Categories</h3>
<ul class="nav nav-pills nav-stacked">
{% for category in object_list %}
<li role="presentation"><a href="#{{ category.category_name|slugify }}">{{ category }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'js/affix.js' %}"></script>
<script>
$(document).ready(function () {
$('.toc-nav').affix({
offset: {
top: ($('.toc-wrapper').offset().top - $('.navbar').height())
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% load training_extras %}
<h4>{{ category }}</h4>
<table class="table">
<thead>
<tr>
<td>Item</td>
<td>Started</td>
<td>Complete</td>
<td>Assessed</td>
</tr>
</thead>
<tbody>
{% for item in category.trainingitem_set.all %}
{% with record=item|item_record:request.user %}
{% include "training/trainingitem_detail_row.html" %}
{% endwith %}
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,31 @@
<tr class="
{% if record.assessed %}
success
{% elif record.complete %}
info
{% elif record.started %}
warning
{% else %}
danger
{% endif %}
">
<td>{{ record.training_item }}</td>
<td>
{{ record.started_date|default_if_none:"No" }} <em>{{ record.started_trainer|default_if_none:"" }}</em>
{% if record.started_notes %}
<pre>{{ record.started_notes }}</pre>
{% endif %}
</td>
<td>
{{ record.completed_date|default_if_none:"No" }} <em>{{ record.completed_trainer|default_if_none:"" }}</em>
{% if record.completed_notes %}
<pre>{{ record.completed_notes }}</pre>
{% endif %}
</td>
<td>
{{ record.assessed_date|default_if_none:"No" }} <em>{{ record.assessed_trainer|default_if_none:"" }}</em>
{% if record.assessed_notes %}
<pre>{{ record.assessed_notes }}</pre>
{% endif %}
</td>
</tr>

View File

@@ -0,0 +1 @@
__author__ = 'Tom Price'

View File

@@ -0,0 +1,14 @@
from django import template
from training import models
__author__ = 'Tom Price'
register = template.Library()
@register.filter
def item_record(item, user):
if item in user.trainingitem_set.all():
return user.trainingrecords.get(training_item=item)
return models.TrainingRecord(training_item=item)

10
training/urls.py Normal file
View File

@@ -0,0 +1,10 @@
__author__ = 'Tom Price'
from django.conf.urls import patterns, url
from training import views
urlpatterns = patterns('',
url(r'^$', views.SelfUserTrainingRecordView.as_view()),
url(r'user/(?P<pk>\d+)/$', views.UserTrainingRecordView.as_view()),
url(r'item/', views.TrainingCategoryItemListView.as_view()),
)

34
training/views.py Normal file
View File

@@ -0,0 +1,34 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.shortcuts import render
from django.views import generic
from training import models
# Create your views here.
class UserTrainingRecordView(generic.DetailView):
model = get_user_model()
template_name = 'training/profile_detail.html'
def get_context_data(self, **kwargs):
context = super(UserTrainingRecordView, self).get_context_data(**kwargs)
context['categories'] = models.TrainingCategory.objects.all()
return context
class SelfUserTrainingRecordView(UserTrainingRecordView):
template_name = 'training/index.html'
def get_queryset(self):
pk = self.request.user.id
self.kwargs['pk'] = pk
return self.model.objects.filter(pk=pk)
class TrainingCategoryItemListView(generic.ListView):
model = models.TrainingCategory
template_name = 'training/trainingcategory_item_list.html'