mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-03 10:38:23 +00:00
Merge branch 'master' into paperwork
This commit is contained in:
0
PyRIGS/formats/__init__.py
Normal file
0
PyRIGS/formats/__init__.py
Normal file
0
PyRIGS/formats/en/__init__.py
Normal file
0
PyRIGS/formats/en/__init__.py
Normal file
5
PyRIGS/formats/en/formats.py
Normal file
5
PyRIGS/formats/en/formats.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
DATETIME_FORMAT = ('d/m/Y H:i')
|
||||
DATE_FORMAT = ('d/m/Y')
|
||||
TIME_FORMAT = ('H:i')
|
||||
@@ -70,22 +70,10 @@ WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
try:
|
||||
import pymysql
|
||||
pymysql.install_as_MySQLdb()
|
||||
except ImportError:
|
||||
pass
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
},
|
||||
'legacy': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'HOST': 'alfie.codedinternet.com',
|
||||
'NAME': 'tec_rigs',
|
||||
'USER': 'tec_rigs',
|
||||
'PASSWORD': 'xMNb(b+Giu]&',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +162,9 @@ else:
|
||||
|
||||
LANGUAGE_CODE = 'en-gb'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = 'Europe/London'
|
||||
|
||||
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@@ -198,7 +188,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||
STATIC_DIRS = (
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.conf import settings
|
||||
from registration.backends.default.views import RegistrationView
|
||||
import RIGS
|
||||
from RIGS import regbackend
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Examples:
|
||||
@@ -12,7 +13,7 @@ urlpatterns = patterns('',
|
||||
|
||||
url(r'^', include('RIGS.urls')),
|
||||
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('registration.backends.default.urls')),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# TEC PA & Lighting - PyRIGS #
|
||||
Welcome to TEC PA & Lightings PyRIGS program. This is a reiplmentation of the exisiting 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.
|
||||
|
||||
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
|
||||
|
||||
@@ -16,7 +16,7 @@ Most of the documents here assume a basic knowledge of how Python and Django wor
|
||||
### Editing ###
|
||||
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
|
||||
|
||||
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steaper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
|
||||
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
|
||||
|
||||
Please contact TJP for details on how to acquire these.
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ class InvoiceEvent(generic.View):
|
||||
|
||||
class PaymentCreate(generic.CreateView):
|
||||
model = models.Payment
|
||||
fields = ['invoice','date','amount','method']
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(generic.CreateView, self).get_initial()
|
||||
|
||||
@@ -12,12 +12,17 @@ from RIGS import models
|
||||
|
||||
#Registration
|
||||
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||
username = forms.CharField(required=True, max_length=30)
|
||||
first_name = forms.CharField(required=False, max_length=50)
|
||||
last_name = forms.CharField(required=False, max_length=50)
|
||||
initials = forms.CharField(required=True, max_length=5)
|
||||
phone = forms.CharField(required=False, max_length=13)
|
||||
captcha = ReCaptchaField()
|
||||
|
||||
class Meta:
|
||||
model = models.Profile
|
||||
fields = ('username','first_name','last_name','initials','phone')
|
||||
|
||||
def clean_initials(self):
|
||||
"""
|
||||
Validate that the supplied initials are unique.
|
||||
|
||||
34
RIGS/ical.py
34
RIGS/ical.py
@@ -2,18 +2,26 @@ from RIGS import models, forms
|
||||
from django_ical.views import ICalFeed
|
||||
from django.db.models import Q
|
||||
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
import datetime
|
||||
import datetime, pytz
|
||||
|
||||
class CalendarICS(ICalFeed):
|
||||
"""
|
||||
A simple event calender
|
||||
"""
|
||||
#Metadata which is passed on to clients
|
||||
product_id = 'PyRIGS'
|
||||
title = 'PyRIGS Calendar'
|
||||
product_id = 'RIGS'
|
||||
title = 'RIGS Calendar'
|
||||
timezone = settings.TIME_ZONE
|
||||
file_name = "rigs.ics"
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
timezone.activate(timezone.UTC)
|
||||
return super(CalendarICS, self).get(*args, **kwargs)
|
||||
|
||||
|
||||
def items(self):
|
||||
#include events from up to 1 year ago
|
||||
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
||||
@@ -45,11 +53,13 @@ class CalendarICS(ICalFeed):
|
||||
def item_start_datetime(self, item):
|
||||
#set start date to the earliest defined time for the event
|
||||
if item.meet_at:
|
||||
startDateTime = item.meet_at.replace(tzinfo=None)
|
||||
startDateTime = item.meet_at
|
||||
elif item.access_at:
|
||||
startDateTime = item.access_at.replace(tzinfo=None)
|
||||
startDateTime = item.access_at
|
||||
elif item.has_start_time:
|
||||
startDateTime = datetime.datetime.combine(item.start_date,item.start_time).replace(tzinfo=None)
|
||||
startDateTime = datetime.datetime.combine(item.start_date,item.start_time)
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
startDateTime = tz.normalize(tz.localize(startDateTime)).astimezone(pytz.timezone(self.timezone))
|
||||
else:
|
||||
startDateTime = item.start_date
|
||||
|
||||
@@ -64,9 +74,11 @@ class CalendarICS(ICalFeed):
|
||||
endDateTime = item.end_date
|
||||
|
||||
if item.has_start_time and item.has_end_time: # don't allow an event with specific end but no specific start
|
||||
endDateTime = datetime.datetime.combine(endDateTime,item.end_time).replace(tzinfo=None)
|
||||
endDateTime = datetime.datetime.combine(endDateTime,item.end_time)
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
endDateTime = tz.normalize(tz.localize(endDateTime)).astimezone(pytz.timezone(self.timezone))
|
||||
elif item.has_end_time: # if there's a start time specified then an end time should also be specified
|
||||
endDateTime = datetime.datetime.combine(endDateTime+datetime.timedelta(days=1),datetime.time(00, 00)).replace(tzinfo=None)
|
||||
endDateTime = datetime.datetime.combine(endDateTime+datetime.timedelta(days=1),datetime.time(00, 00))
|
||||
#elif item.end_time: # end time but no start time - this is weird - don't think ICS will like it so ignoring
|
||||
# do nothing
|
||||
|
||||
@@ -90,7 +102,7 @@ class CalendarICS(ICalFeed):
|
||||
|
||||
desc += '\n'
|
||||
if item.meet_at:
|
||||
desc += 'Crew Meet = ' + item.meet_at.strftime('%Y-%m-%d %H:%M') + (('('+item.meet_info+')') if item.meet_info else '---') + '\n'
|
||||
desc += 'Crew Meet = ' + (item.meet_at.strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||
if item.access_at:
|
||||
desc += 'Access At = ' + item.access_at.strftime('%Y-%m-%d %H:%M') + '\n'
|
||||
if item.start_date:
|
||||
@@ -104,7 +116,7 @@ class CalendarICS(ICalFeed):
|
||||
if item.notes:
|
||||
desc += 'Notes:\n'+item.notes+'\n\n'
|
||||
|
||||
base_url = "https://pyrigs.nottinghamtec.co.uk"
|
||||
base_url = "http://rigs.nottinghamtec.co.uk"
|
||||
desc += 'URL = '+base_url+str(item.get_absolute_url())
|
||||
|
||||
return desc
|
||||
@@ -118,7 +130,7 @@ class CalendarICS(ICalFeed):
|
||||
# return ''
|
||||
|
||||
def item_updated(self, item): # some ical clients will display this
|
||||
return item.last_edited_at.replace(tzinfo=None)
|
||||
return item.last_edited_at
|
||||
|
||||
def item_guid(self, item): # use the rig-id as the ical unique event identifier
|
||||
return item.pk
|
||||
66
RIGS/migrations/0023_auto_20150529_0048.py
Normal file
66
RIGS/migrations/0023_auto_20150529_0048.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
import django.contrib.auth.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0022_auto_20150424_2104'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='profile',
|
||||
options={'permissions': (('view_profile', 'Can view Profile'),)},
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='profile',
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='collector',
|
||||
field=models.CharField(max_length=255, null=True, verbose_name=b'collected by', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, verbose_name='email address', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='last_login',
|
||||
field=models.DateTimeField(null=True, verbose_name='last login', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='username',
|
||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, null=True, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -14,6 +14,7 @@ from django.core.urlresolvers import reverse_lazy
|
||||
from decimal import Decimal
|
||||
|
||||
# Create your models here.
|
||||
@python_2_unicode_compatible
|
||||
class Profile(AbstractUser):
|
||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||
@@ -37,17 +38,36 @@ class Profile(AbstractUser):
|
||||
def name(self):
|
||||
return self.get_full_name() + ' "' + self.initials + '"'
|
||||
|
||||
@property
|
||||
def latest_events(self):
|
||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_profile', 'Can view Profile'),
|
||||
)
|
||||
|
||||
class RevisionMixin(object):
|
||||
@property
|
||||
def last_edited_at(self):
|
||||
version = reversion.get_for_object(self)[0]
|
||||
return version.revision.date_created
|
||||
versions = reversion.get_for_object(self)
|
||||
if versions:
|
||||
version = reversion.get_for_object(self)[0]
|
||||
return version.revision.date_created
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def last_edited_by(self):
|
||||
version = reversion.get_for_object(self)[0]
|
||||
return version.revision.user
|
||||
|
||||
versions = reversion.get_for_object(self)
|
||||
if versions:
|
||||
version = reversion.get_for_object(self)[0]
|
||||
return version.revision.user
|
||||
else:
|
||||
return None
|
||||
|
||||
@reversion.register
|
||||
@python_2_unicode_compatible
|
||||
@@ -62,8 +82,9 @@ class Person(models.Model, RevisionMixin):
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
if len(self.notes) > 0:
|
||||
string += "*"
|
||||
if self.notes is not None:
|
||||
if len(self.notes) > 0:
|
||||
string += "*"
|
||||
return string
|
||||
|
||||
@property
|
||||
@@ -78,6 +99,9 @@ class Person(models.Model, RevisionMixin):
|
||||
def latest_events(self):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_person', 'Can view Persons'),
|
||||
@@ -98,8 +122,9 @@ class Organisation(models.Model, RevisionMixin):
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
if len(self.notes) > 0:
|
||||
string += "*"
|
||||
if self.notes is not None:
|
||||
if len(self.notes) > 0:
|
||||
string += "*"
|
||||
return string
|
||||
|
||||
@property
|
||||
@@ -114,6 +139,9 @@ class Organisation(models.Model, RevisionMixin):
|
||||
def latest_events(self):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_organisation', 'Can view Organisations'),
|
||||
@@ -176,6 +204,9 @@ class Venue(models.Model, RevisionMixin):
|
||||
def latest_events(self):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_venue', 'Can view Venues'),
|
||||
@@ -185,14 +216,10 @@ class Venue(models.Model, RevisionMixin):
|
||||
class EventManager(models.Manager):
|
||||
def current_events(self):
|
||||
events = self.filter(
|
||||
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Ends after
|
||||
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Active dry hire
|
||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after
|
||||
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire
|
||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||
models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||
return events
|
||||
@@ -256,7 +283,7 @@ class Event(models.Model, RevisionMixin):
|
||||
payment_method = models.CharField(max_length=255, blank=True, null=True)
|
||||
payment_received = models.CharField(max_length=255, blank=True, null=True)
|
||||
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
|
||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='Collected by')
|
||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
|
||||
|
||||
# Calculated values
|
||||
"""
|
||||
@@ -275,7 +302,9 @@ class Event(models.Model, RevisionMixin):
|
||||
#total = 0.0
|
||||
#for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
|
||||
# total += item.sum
|
||||
total = EventItem.objects.filter(event=self).aggregate(sum_total=models.Sum('cost',field="quantity * cost"))['sum_total']
|
||||
total = EventItem.objects.filter(event=self).aggregate(
|
||||
sum_total=models.Sum(models.F('cost')*models.F('quantity'), output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||
)['sum_total']
|
||||
if total:
|
||||
return total
|
||||
return Decimal("0.00")
|
||||
|
||||
13
RIGS/regbackend.py
Normal file
13
RIGS/regbackend.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from RIGS.models import Profile
|
||||
from RIGS.forms import ProfileRegistrationFormUniqueEmail
|
||||
|
||||
def user_created(sender, user, request, **kwargs):
|
||||
form = ProfileRegistrationFormUniqueEmail(request.POST)
|
||||
user.first_name = form.data['first_name']
|
||||
user.last_name = form.data['last_name']
|
||||
user.initials = form.data['initials']
|
||||
user.phone = form.data['phone']
|
||||
user.save()
|
||||
|
||||
from registration.signals import user_registered
|
||||
user_registered.connect(user_created)
|
||||
@@ -90,20 +90,24 @@ class EventPrint(generic.View):
|
||||
object = get_object_or_404(models.Event, pk=pk)
|
||||
template = get_template('RIGS/event_print.xml')
|
||||
copies = ('TEC', 'Client')
|
||||
context = RequestContext(request, {
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
merger = PdfFileMerger()
|
||||
|
||||
for copy in copies:
|
||||
context['copy'] = copy
|
||||
|
||||
context = RequestContext(request, { # this should be outside the loop, but bug in 1.8.2 prevents this
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'copy':copy
|
||||
})
|
||||
|
||||
# context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
|
||||
|
||||
rml = template.render(context)
|
||||
buffer = StringIO.StringIO()
|
||||
|
||||
@@ -126,6 +130,7 @@ class EventPrint(generic.View):
|
||||
|
||||
|
||||
class EventDuplicate(generic.RedirectView):
|
||||
permanent = False;
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
new = get_object_or_404(models.Event, pk=kwargs['pk'])
|
||||
new.pk = None
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/* Welcome to Compass. Use this file to write IE specific override styles.
|
||||
* Import this file using the following HTML or equivalent:
|
||||
* <!--[if IE]>
|
||||
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
|
||||
* <![endif]--> */
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
73
RIGS/static/js/asteroids.min.js
vendored
Normal file
73
RIGS/static/js/asteroids.min.js
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
(function(){function Asteroids(){if(!window.ASTEROIDS)
|
||||
window.ASTEROIDS={enemiesKilled:0};function Vector(x,y){if(typeof x=='Object'){this.x=x.x;this.y=x.y;}else{this.x=x;this.y=y;}};Vector.prototype={cp:function(){return new Vector(this.x,this.y);},mul:function(factor){this.x*=factor;this.y*=factor;return this;},mulNew:function(factor){return new Vector(this.x*factor,this.y*factor);},add:function(vec){this.x+=vec.x;this.y+=vec.y;return this;},addNew:function(vec){return new Vector(this.x+vec.x,this.y+vec.y);},sub:function(vec){this.x-=vec.x;this.y-=vec.y;return this;},subNew:function(vec){return new Vector(this.x-vec.x,this.y-vec.y);},rotate:function(angle){var x=this.x,y=this.y;this.x=x*Math.cos(angle)-Math.sin(angle)*y;this.y=x*Math.sin(angle)+Math.cos(angle)*y;return this;},rotateNew:function(angle){return this.cp().rotate(angle);},setAngle:function(angle){var l=this.len();this.x=Math.cos(angle)*l;this.y=Math.sin(angle)*l;return this;},setAngleNew:function(angle){return this.cp().setAngle(angle);},setLength:function(length){var l=this.len();if(l)this.mul(length/l);else this.x=this.y=length;return this;},setLengthNew:function(length){return this.cp().setLength(length);},normalize:function(){var l=this.len();this.x/=l;this.y/=l;return this;},normalizeNew:function(){return this.cp().normalize();},angle:function(){return Math.atan2(this.y,this.x);},collidesWith:function(rect){return this.x>rect.x&&this.y>rect.y&&this.x<rect.x+rect.width&&this.y<rect.y+rect.height;},len:function(){var l=Math.sqrt(this.x*this.x+this.y*this.y);if(l<0.005&&l>-0.005)return 0;return l;},is:function(test){return typeof test=='object'&&this.x==test.x&&this.y==test.y;},toString:function(){return'[Vector('+this.x+', '+this.y+') angle: '+this.angle()+', length: '+this.len()+']';}};function Line(p1,p2){this.p1=p1;this.p2=p2;};Line.prototype={shift:function(pos){this.p1.add(pos);this.p2.add(pos);},intersectsWithRect:function(rect){var LL=new Vector(rect.x,rect.y+rect.height);var UL=new Vector(rect.x,rect.y);var LR=new Vector(rect.x+rect.width,rect.y+rect.height);var UR=new Vector(rect.x+rect.width,rect.y);if(this.p1.x>LL.x&&this.p1.x<UR.x&&this.p1.y<LL.y&&this.p1.y>UR.y&&this.p2.x>LL.x&&this.p2.x<UR.x&&this.p2.y<LL.y&&this.p2.y>UR.y)return true;if(this.intersectsLine(new Line(UL,LL)))return true;if(this.intersectsLine(new Line(LL,LR)))return true;if(this.intersectsLine(new Line(UL,UR)))return true;if(this.intersectsLine(new Line(UR,LR)))return true;return false;},intersectsLine:function(line2){var v1=this.p1,v2=this.p2;var v3=line2.p1,v4=line2.p2;var denom=((v4.y-v3.y)*(v2.x-v1.x))-((v4.x-v3.x)*(v2.y-v1.y));var numerator=((v4.x-v3.x)*(v1.y-v3.y))-((v4.y-v3.y)*(v1.x-v3.x));var numerator2=((v2.x-v1.x)*(v1.y-v3.y))-((v2.y-v1.y)*(v1.x-v3.x));if(denom==0.0){return false;}
|
||||
var ua=numerator/denom;var ub=numerator2/denom;return(ua>=0.0&&ua<=1.0&&ub>=0.0&&ub<=1.0);}};var that=this;var isIE=!!window.ActiveXObject;var isIEQuirks=isIE&&document.compatMode=="BackCompat";var w=document.documentElement.clientWidth,h=document.documentElement.clientHeight;if(isIEQuirks){w=document.body.clientWidth;h=document.body.clientHeight;}
|
||||
var playerWidth=20,playerHeight=30;var playerVerts=[[-1*playerHeight/2,-1*playerWidth/2],[-1*playerHeight/2,playerWidth/2],[playerHeight/2,0]];var ignoredTypes=['HTML','HEAD','BODY','SCRIPT','TITLE','META','STYLE','LINK'];if(window.ActiveXObject)
|
||||
ignoredTypes=['HTML','HEAD','BODY','SCRIPT','TITLE','META','STYLE','LINK','SHAPE','LINE','GROUP','IMAGE','STROKE','FILL','SKEW','PATH','TEXTPATH','INS'];var hiddenTypes=['BR','HR'];var FPS=50;var acc=300;var maxSpeed=600;var rotSpeed=360;var bulletSpeed=700;var particleSpeed=400;var timeBetweenFire=150;var timeBetweenBlink=250;var timeBetweenEnemyUpdate=isIE?10000:2000;var bulletRadius=2;var maxParticles=isIE?20:40;var maxBullets=isIE?10:20;this.flame={r:[],y:[]};this.toggleBlinkStyle=function(){if(this.updated.blink.isActive){removeClass(document.body,'ASTEROIDSBLINK');}else{addClass(document.body,'ASTEROIDSBLINK');}
|
||||
this.updated.blink.isActive=!this.updated.blink.isActive;};addStylesheet(".ASTEROIDSBLINK .ASTEROIDSYEAHENEMY","outline: 2px dotted red;");this.pos=new Vector(100,100);this.lastPos=false;this.vel=new Vector(0,0);this.dir=new Vector(0,1);this.keysPressed={};this.firedAt=false;this.updated={enemies:false,flame:new Date().getTime(),blink:{time:0,isActive:false}};this.scrollPos=new Vector(0,0);this.bullets=[];this.enemies=[];this.dying=[];this.totalEnemies=0;this.particles=[];function updateEnemyIndex(){for(var i=0,enemy;enemy=that.enemies[i];i++)
|
||||
removeClass(enemy,"ASTEROIDSYEAHENEMY");var all=document.body.getElementsByTagName('*');that.enemies=[];for(var i=0,el;el=all[i];i++){if(indexOf(ignoredTypes,el.tagName.toUpperCase())==-1&&el.prefix!='g_vml_'&&hasOnlyTextualChildren(el)&&el.className!="ASTEROIDSYEAH"&&el.offsetHeight>0){el.aSize=size(el);that.enemies.push(el);addClass(el,"ASTEROIDSYEAHENEMY");if(!el.aAdded){el.aAdded=true;that.totalEnemies++;}}}};updateEnemyIndex();var createFlames;(function(){var rWidth=playerWidth,rIncrease=playerWidth*0.1,yWidth=playerWidth*0.6,yIncrease=yWidth*0.2,halfR=rWidth/2,halfY=yWidth/2,halfPlayerHeight=playerHeight/2;createFlames=function(){that.flame.r=[[-1*halfPlayerHeight,-1*halfR]];that.flame.y=[[-1*halfPlayerHeight,-1*halfY]];for(var x=0;x<rWidth;x+=rIncrease){that.flame.r.push([-random(2,7)-halfPlayerHeight,x-halfR]);}
|
||||
that.flame.r.push([-1*halfPlayerHeight,halfR]);for(var x=0;x<yWidth;x+=yIncrease){that.flame.y.push([-random(2,7)-halfPlayerHeight,x-halfY]);}
|
||||
that.flame.y.push([-1*halfPlayerHeight,halfY]);};})();createFlames();function radians(deg){return deg*0.0174532925;};function degrees(rad){return rad*57.2957795;};function random(from,to){return Math.floor(Math.random()*(to+1)+from);};function code(name){var table={'up':38,'down':40,'left':37,'right':39,'esc':27};if(table[name])return table[name];return name.charCodeAt(0);};function boundsCheck(vec){if(vec.x>w)
|
||||
vec.x=0;else if(vec.x<0)
|
||||
vec.x=w;if(vec.y>h)
|
||||
vec.y=0;else if(vec.y<0)
|
||||
vec.y=h;};function size(element){var el=element,left=0,top=0;do{left+=el.offsetLeft||0;top+=el.offsetTop||0;el=el.offsetParent;}while(el);return{x:left,y:top,width:element.offsetWidth||10,height:element.offsetHeight||10};};function addEvent(obj,type,fn){if(obj.addEventListener)
|
||||
obj.addEventListener(type,fn,false);else if(obj.attachEvent){obj["e"+type+fn]=fn;obj[type+fn]=function(){obj["e"+type+fn](window.event);}
|
||||
obj.attachEvent("on"+type,obj[type+fn]);}}
|
||||
function removeEvent(obj,type,fn){if(obj.removeEventListener)
|
||||
obj.removeEventListener(type,fn,false);else if(obj.detachEvent){obj.detachEvent("on"+type,obj[type+fn]);obj[type+fn]=null;obj["e"+type+fn]=null;}}
|
||||
function arrayRemove(array,from,to){var rest=array.slice((to||from)+1||array.length);array.length=from<0?array.length+from:from;return array.push.apply(array,rest);};function applyVisibility(vis){for(var i=0,p;p=window.ASTEROIDSPLAYERS[i];i++){p.gameContainer.style.visibility=vis;}}
|
||||
function getElementFromPoint(x,y){applyVisibility('hidden');var element=document.elementFromPoint(x,y);if(!element){applyVisibility('visible');return false;}
|
||||
if(element.nodeType==3)
|
||||
element=element.parentNode;applyVisibility('visible');return element;};function addParticles(startPos){var time=new Date().getTime();var amount=maxParticles;for(var i=0;i<amount;i++){that.particles.push({dir:(new Vector(Math.random()*20-10,Math.random()*20-10)).normalize(),pos:startPos.cp(),cameAlive:time});}};function setScore(){that.points.innerHTML=window.ASTEROIDS.enemiesKilled*10;};function hasOnlyTextualChildren(element){if(element.offsetLeft<-100&&element.offsetWidth>0&&element.offsetHeight>0)return false;if(indexOf(hiddenTypes,element.tagName)!=-1)return true;if(element.offsetWidth==0&&element.offsetHeight==0)return false;for(var i=0;i<element.childNodes.length;i++){if(indexOf(hiddenTypes,element.childNodes[i].tagName)==-1&&element.childNodes[i].childNodes.length!=0)return false;}
|
||||
return true;};function indexOf(arr,item,from){if(arr.indexOf)return arr.indexOf(item,from);var len=arr.length;for(var i=(from<0)?Math.max(0,len+from):from||0;i<len;i++){if(arr[i]===item)return i;}
|
||||
return-1;};function addClass(element,className){if(element.className.indexOf(className)==-1)
|
||||
element.className=(element.className+' '+className).replace(/\s+/g,' ').replace(/^\s+|\s+$/g,'');};function removeClass(element,className){element.className=element.className.replace(new RegExp('(^|\\s)'+className+'(?:\\s|$)'),'$1');};function addStylesheet(selector,rules){var stylesheet=document.createElement('style');stylesheet.type='text/css';stylesheet.rel='stylesheet';stylesheet.id='ASTEROIDSYEAHSTYLES';try{stylesheet.innerHTML=selector+"{"+rules+"}";}catch(e){stylesheet.styleSheet.addRule(selector,rules);}
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);};function removeStylesheet(name){var stylesheet=document.getElementById(name);if(stylesheet){stylesheet.parentNode.removeChild(stylesheet);}};this.gameContainer=document.createElement('div');this.gameContainer.className='ASTEROIDSYEAH';document.body.appendChild(this.gameContainer);this.canvas=document.createElement('canvas');this.canvas.setAttribute('width',w);this.canvas.setAttribute('height',h);this.canvas.className='ASTEROIDSYEAH';with(this.canvas.style){width=w+"px";height=h+"px";position="fixed";top="0px";left="0px";bottom="0px";right="0px";zIndex="10000";}
|
||||
if(typeof G_vmlCanvasManager!='undefined'){this.canvas=G_vmlCanvasManager.initElement(this.canvas);if(!this.canvas.getContext){alert("So... you're using IE? Please join me at http://github.com/erkie/erkie.github.com if you think you can help");}}else{if(!this.canvas.getContext){alert('This program does not yet support your browser. Please join me at http://github.com/erkie/erkie.github.com if you think you can help');}}
|
||||
addEvent(this.canvas,'mousedown',function(e){e=e||window.event;var message=document.createElement('span');message.style.position='absolute';message.style.border='1px solid #999';message.style.background='white';message.style.color="black";message.innerHTML='Press Esc to quit';document.body.appendChild(message);var x=e.pageX||(e.clientX+document.documentElement.scrollLeft);var y=e.pageY||(e.clientY+document.documentElement.scrollTop);message.style.left=x-message.offsetWidth/2+'px';message.style.top=y-message.offsetHeight/2+'px';setTimeout(function(){try{message.parentNode.removeChild(message);}catch(e){}},1000);});var eventResize=function(){if(!isIE){that.canvas.style.display="none";w=document.documentElement.clientWidth;h=document.documentElement.clientHeight;that.canvas.setAttribute('width',w);that.canvas.setAttribute('height',h);with(that.canvas.style){display="block";width=w+"px";height=h+"px";}}else{w=document.documentElement.clientWidth;h=document.documentElement.clientHeight;if(isIEQuirks){w=document.body.clientWidth;h=document.body.clientHeight;}
|
||||
that.canvas.setAttribute('width',w);that.canvas.setAttribute('height',h);}};addEvent(window,'resize',eventResize);this.gameContainer.appendChild(this.canvas);this.ctx=this.canvas.getContext("2d");this.ctx.fillStyle="black";this.ctx.strokeStyle="black";if(!document.getElementById('ASTEROIDS-NAVIGATION')){this.navigation=document.createElement('div');this.navigation.id="ASTEROIDS-NAVIGATION";this.navigation.className="ASTEROIDSYEAH";with(this.navigation.style){fontFamily="Arial,sans-serif";position="fixed";zIndex="10001";bottom="10px";right="10px";textAlign="right";}
|
||||
this.navigation.innerHTML="(press esc to quit) ";this.gameContainer.appendChild(this.navigation);this.points=document.createElement('span');this.points.id='ASTEROIDS-POINTS';this.points.style.font="28pt Arial, sans-serif";this.points.style.fontWeight="bold";this.points.className="ASTEROIDSYEAH";this.navigation.appendChild(this.points);}else{this.navigation=document.getElementById('ASTEROIDS-NAVIGATION');this.points=document.getElementById('ASTEROIDS-POINTS');}
|
||||
if(isIEQuirks){this.gameContainer.style.position=this.canvas.style.position=this.navigation.style.position="absolute";}
|
||||
setScore();if(typeof G_vmlCanvasManager!='undefined'){var children=this.canvas.getElementsByTagName('*');for(var i=0,c;c=children[i];i++)
|
||||
addClass(c,'ASTEROIDSYEAH');}
|
||||
var eventKeydown=function(event){event=event||window.event;that.keysPressed[event.keyCode]=true;switch(event.keyCode){case code(' '):that.firedAt=1;break;}
|
||||
if(indexOf([code('up'),code('down'),code('right'),code('left'),code(' '),code('B'),code('W'),code('A'),code('S'),code('D')],event.keyCode)!=-1){if(event.preventDefault)
|
||||
event.preventDefault();if(event.stopPropagation)
|
||||
event.stopPropagation();event.returnValue=false;event.cancelBubble=true;return false;}};addEvent(document,'keydown',eventKeydown);var eventKeypress=function(event){event=event||window.event;if(indexOf([code('up'),code('down'),code('right'),code('left'),code(' '),code('W'),code('A'),code('S'),code('D')],event.keyCode||event.which)!=-1){if(event.preventDefault)
|
||||
event.preventDefault();if(event.stopPropagation)
|
||||
event.stopPropagation();event.returnValue=false;event.cancelBubble=true;return false;}};addEvent(document,'keypress',eventKeypress);var eventKeyup=function(event){event=event||window.event;that.keysPressed[event.keyCode]=false;if(indexOf([code('up'),code('down'),code('right'),code('left'),code(' '),code('B'),code('W'),code('A'),code('S'),code('D')],event.keyCode)!=-1){if(event.preventDefault)
|
||||
event.preventDefault();if(event.stopPropagation)
|
||||
event.stopPropagation();event.returnValue=false;event.cancelBubble=true;return false;}};addEvent(document,'keyup',eventKeyup);this.ctx.clear=function(){this.clearRect(0,0,w,h);};this.ctx.clear();this.ctx.drawLine=function(xFrom,yFrom,xTo,yTo){this.beginPath();this.moveTo(xFrom,yFrom);this.lineTo(xTo,yTo);this.lineTo(xTo+1,yTo+1);this.closePath();this.fill();};this.ctx.tracePoly=function(verts){this.beginPath();this.moveTo(verts[0][0],verts[0][1]);for(var i=1;i<verts.length;i++)
|
||||
this.lineTo(verts[i][0],verts[i][1]);this.closePath();};this.ctx.drawPlayer=function(){this.save();this.translate(that.pos.x,that.pos.y);this.rotate(that.dir.angle());this.tracePoly(playerVerts);this.fillStyle="white";this.fill();this.tracePoly(playerVerts);this.stroke();this.restore();};var PI_SQ=Math.PI*2;this.ctx.drawBullets=function(bullets){for(var i=0;i<bullets.length;i++){this.beginPath();this.arc(bullets[i].pos.x,bullets[i].pos.y,bulletRadius,0,PI_SQ,true);this.closePath();this.fill();}};var randomParticleColor=function(){return(['red','yellow'])[random(0,1)];};this.ctx.drawParticles=function(particles){var oldColor=this.fillStyle;for(var i=0;i<particles.length;i++){this.fillStyle=randomParticleColor();this.drawLine(particles[i].pos.x,particles[i].pos.y,particles[i].pos.x-particles[i].dir.x*10,particles[i].pos.y-particles[i].dir.y*10);}
|
||||
this.fillStyle=oldColor;};this.ctx.drawFlames=function(flame){this.save();this.translate(that.pos.x,that.pos.y);this.rotate(that.dir.angle());var oldColor=this.strokeStyle;this.strokeStyle="red";this.tracePoly(flame.r);this.stroke();this.strokeStyle="yellow";this.tracePoly(flame.y);this.stroke();this.strokeStyle=oldColor;this.restore();}
|
||||
try{window.focus();}catch(e){}
|
||||
addParticles(this.pos);addClass(document.body,'ASTEROIDSYEAH');var isRunning=true;var lastUpdate=new Date().getTime();this.update=function(){var forceChange=false;var nowTime=new Date().getTime();var tDelta=(nowTime-lastUpdate)/1000;lastUpdate=nowTime;var drawFlame=false;if(nowTime-this.updated.flame>50){createFlames();this.updated.flame=nowTime;}
|
||||
this.scrollPos.x=window.pageXOffset||document.documentElement.scrollLeft;this.scrollPos.y=window.pageYOffset||document.documentElement.scrollTop;if((this.keysPressed[code('up')])||(this.keysPressed[code('W')])){this.vel.add(this.dir.mulNew(acc*tDelta));drawFlame=true;}else{this.vel.mul(0.96);}
|
||||
if((this.keysPressed[code('left')])||(this.keysPressed[code('A')])){forceChange=true;this.dir.rotate(radians(rotSpeed*tDelta*-1));}
|
||||
if((this.keysPressed[code('right')])||(this.keysPressed[code('D')])){forceChange=true;this.dir.rotate(radians(rotSpeed*tDelta));}
|
||||
if(this.keysPressed[code(' ')]&&nowTime-this.firedAt>timeBetweenFire){this.bullets.unshift({'dir':this.dir.cp(),'pos':this.pos.cp(),'startVel':this.vel.cp(),'cameAlive':nowTime});this.firedAt=nowTime;if(this.bullets.length>maxBullets){this.bullets.pop();}}
|
||||
if(this.keysPressed[code('B')]){if(!this.updated.enemies){updateEnemyIndex();this.updated.enemies=true;}
|
||||
forceChange=true;this.updated.blink.time+=tDelta*1000;if(this.updated.blink.time>timeBetweenBlink){this.toggleBlinkStyle();this.updated.blink.time=0;}}else{this.updated.enemies=false;}
|
||||
if(this.keysPressed[code('esc')]){destroy.apply(this);return;}
|
||||
if(this.vel.len()>maxSpeed){this.vel.setLength(maxSpeed);}
|
||||
this.pos.add(this.vel.mulNew(tDelta));if(this.pos.x>w){window.scrollTo(this.scrollPos.x+50,this.scrollPos.y);this.pos.x=0;}else if(this.pos.x<0){window.scrollTo(this.scrollPos.x-50,this.scrollPos.y);this.pos.x=w;}
|
||||
if(this.pos.y>h){window.scrollTo(this.scrollPos.x,this.scrollPos.y+h*0.75);this.pos.y=0;}else if(this.pos.y<0){window.scrollTo(this.scrollPos.x,this.scrollPos.y-h*0.75);this.pos.y=h;}
|
||||
for(var i=this.bullets.length-1;i>=0;i--){if(nowTime-this.bullets[i].cameAlive>2000){this.bullets.splice(i,1);forceChange=true;continue;}
|
||||
var bulletVel=this.bullets[i].dir.setLengthNew(bulletSpeed*tDelta).add(this.bullets[i].startVel.mulNew(tDelta));this.bullets[i].pos.add(bulletVel);boundsCheck(this.bullets[i].pos);var murdered=getElementFromPoint(this.bullets[i].pos.x,this.bullets[i].pos.y);if(murdered&&murdered.tagName&&indexOf(ignoredTypes,murdered.tagName.toUpperCase())==-1&&hasOnlyTextualChildren(murdered)&&murdered.className!="ASTEROIDSYEAH"){didKill=true;addParticles(this.bullets[i].pos);this.dying.push(murdered);this.bullets.splice(i,1);continue;}}
|
||||
if(this.dying.length){for(var i=this.dying.length-1;i>=0;i--){try{if(this.dying[i].parentNode)
|
||||
window.ASTEROIDS.enemiesKilled++;this.dying[i].parentNode.removeChild(this.dying[i]);}catch(e){}}
|
||||
setScore();this.dying=[];}
|
||||
for(var i=this.particles.length-1;i>=0;i--){this.particles[i].pos.add(this.particles[i].dir.mulNew(particleSpeed*tDelta*Math.random()));if(nowTime-this.particles[i].cameAlive>1000){this.particles.splice(i,1);forceChange=true;continue;}}
|
||||
if(isIEQuirks){this.gameContainer.style.left=this.canvas.style.left=document.documentElement.scrollLeft+"px";this.gameContainer.style.top=this.canvas.style.top=document.documentElement.scrollTop+"px";this.navigation.style.right="10px";this.navigation.style.top=document.documentElement.scrollTop+document.body.clientHeight-this.navigation.clientHeight-10+"px";}
|
||||
if(forceChange||this.bullets.length!=0||this.particles.length!=0||!this.pos.is(this.lastPos)||this.vel.len()>0){this.ctx.clear();this.ctx.drawPlayer();if(drawFlame)
|
||||
this.ctx.drawFlames(that.flame);if(this.bullets.length){this.ctx.drawBullets(this.bullets);}
|
||||
if(this.particles.length){this.ctx.drawParticles(this.particles);}}
|
||||
this.lastPos=this.pos;}
|
||||
var updateFunc=function(){try{that.update.call(that);}
|
||||
catch(e){clearInterval(interval);throw e;}};var interval=setInterval(updateFunc,1000/FPS);function destroy(){removeEvent(document,'keydown',eventKeydown);removeEvent(document,'keypress',eventKeypress);removeEvent(document,'keyup',eventKeyup);removeEvent(window,'resize',eventResize);isRunning=false;removeStylesheet("ASTEROIDSYEAHSTYLES");removeClass(document.body,'ASTEROIDSYEAH');this.gameContainer.parentNode.removeChild(this.gameContainer);};}
|
||||
if(!window.ASTEROIDSPLAYERS)
|
||||
window.ASTEROIDSPLAYERS=[];if(window.ActiveXObject&&!document.createElement('canvas').getContext){try{var xamlScript=document.createElement('script');xamlScript.setAttribute('type','text/xaml');xamlScript.textContent='<?xml version="1.0"?><Canvas xmlns="http://schemas.microsoft.com/client/2007"></Canvas>';document.getElementsByTagName('head')[0].appendChild(xamlScript);}catch(e){}
|
||||
var script=document.createElement("script");script.setAttribute('type','text/javascript');script.onreadystatechange=function(){if(script.readyState=='loaded'||script.readyState=='complete'){if(typeof G_vmlCanvasManager!="undefined")
|
||||
window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids();}};script.src="http://erkie.github.com/excanvas.js";document.getElementsByTagName('head')[0].appendChild(script);}
|
||||
else window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids();})();
|
||||
105
RIGS/static/js/konami.js
Executable file
105
RIGS/static/js/konami.js
Executable file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Konami-JS ~
|
||||
* :: Now with support for touch events and multiple instances for
|
||||
* :: those situations that call for multiple easter eggs!
|
||||
* Code: http://konami-js.googlecode.com/
|
||||
* Examples: http://www.snaptortoise.com/konami-js
|
||||
* Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
|
||||
* Version: 1.4.2 (9/2/2013)
|
||||
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
|
||||
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1 and Dolphin Browser
|
||||
*/
|
||||
|
||||
var Konami = function (callback) {
|
||||
var konami = {
|
||||
addEvent: function (obj, type, fn, ref_obj) {
|
||||
if (obj.addEventListener)
|
||||
obj.addEventListener(type, fn, false);
|
||||
else if (obj.attachEvent) {
|
||||
// IE
|
||||
obj["e" + type + fn] = fn;
|
||||
obj[type + fn] = function () {
|
||||
obj["e" + type + fn](window.event, ref_obj);
|
||||
}
|
||||
obj.attachEvent("on" + type, obj[type + fn]);
|
||||
}
|
||||
},
|
||||
input: "",
|
||||
pattern: "38384040373937396665",
|
||||
load: function (link) {
|
||||
this.addEvent(document, "keydown", function (e, ref_obj) {
|
||||
if (ref_obj) konami = ref_obj; // IE
|
||||
konami.input += e ? e.keyCode : event.keyCode;
|
||||
if (konami.input.length > konami.pattern.length)
|
||||
konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
|
||||
if (konami.input == konami.pattern) {
|
||||
konami.code(link);
|
||||
konami.input = "";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}, this);
|
||||
this.iphone.load(link);
|
||||
},
|
||||
code: function (link) {
|
||||
window.location = link
|
||||
},
|
||||
iphone: {
|
||||
start_x: 0,
|
||||
start_y: 0,
|
||||
stop_x: 0,
|
||||
stop_y: 0,
|
||||
tap: false,
|
||||
capture: false,
|
||||
orig_keys: "",
|
||||
keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
|
||||
code: function (link) {
|
||||
konami.code(link);
|
||||
},
|
||||
load: function (link) {
|
||||
this.orig_keys = this.keys;
|
||||
konami.addEvent(document, "touchmove", function (e) {
|
||||
if (e.touches.length == 1 && konami.iphone.capture == true) {
|
||||
var touch = e.touches[0];
|
||||
konami.iphone.stop_x = touch.pageX;
|
||||
konami.iphone.stop_y = touch.pageY;
|
||||
konami.iphone.tap = false;
|
||||
konami.iphone.capture = false;
|
||||
konami.iphone.check_direction();
|
||||
}
|
||||
});
|
||||
konami.addEvent(document, "touchend", function (evt) {
|
||||
if (konami.iphone.tap == true) konami.iphone.check_direction(link);
|
||||
}, false);
|
||||
konami.addEvent(document, "touchstart", function (evt) {
|
||||
konami.iphone.start_x = evt.changedTouches[0].pageX;
|
||||
konami.iphone.start_y = evt.changedTouches[0].pageY;
|
||||
konami.iphone.tap = true;
|
||||
konami.iphone.capture = true;
|
||||
});
|
||||
},
|
||||
check_direction: function (link) {
|
||||
x_magnitude = Math.abs(this.start_x - this.stop_x);
|
||||
y_magnitude = Math.abs(this.start_y - this.stop_y);
|
||||
x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
|
||||
y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
|
||||
result = (x_magnitude > y_magnitude) ? x : y;
|
||||
result = (this.tap == true) ? "TAP" : result;
|
||||
|
||||
if (result == this.keys[0]) this.keys = this.keys.slice(1, this.keys.length);
|
||||
if (this.keys.length == 0) {
|
||||
this.keys = this.orig_keys;
|
||||
this.code(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeof callback === "string" && konami.load(callback);
|
||||
if (typeof callback === "function") {
|
||||
konami.code = callback;
|
||||
konami.load();
|
||||
}
|
||||
|
||||
return konami;
|
||||
};
|
||||
86
RIGS/static/js/moment-twitter.js
Normal file
86
RIGS/static/js/moment-twitter.js
Normal file
@@ -0,0 +1,86 @@
|
||||
(function() {
|
||||
var day, formats, hour, initialize, minute, second, week;
|
||||
|
||||
second = 1e3;
|
||||
|
||||
minute = 6e4;
|
||||
|
||||
hour = 36e5;
|
||||
|
||||
day = 864e5;
|
||||
|
||||
week = 6048e5;
|
||||
|
||||
formats = {
|
||||
seconds: {
|
||||
short: 's',
|
||||
long: ' sec'
|
||||
},
|
||||
minutes: {
|
||||
short: 'm',
|
||||
long: ' min'
|
||||
},
|
||||
hours: {
|
||||
short: 'h',
|
||||
long: ' hr'
|
||||
},
|
||||
days: {
|
||||
short: 'd',
|
||||
long: ' day'
|
||||
}
|
||||
};
|
||||
|
||||
initialize = function(moment) {
|
||||
var twitterFormat;
|
||||
twitterFormat = function(format) {
|
||||
var diff, num, unit, unitStr;
|
||||
diff = Math.abs(this.diff(moment()));
|
||||
unit = null;
|
||||
num = null;
|
||||
if (diff <= second) {
|
||||
unit = 'seconds';
|
||||
num = 1;
|
||||
} else if (diff < minute) {
|
||||
unit = 'seconds';
|
||||
} else if (diff < hour) {
|
||||
unit = 'minutes';
|
||||
} else if (diff < day) {
|
||||
unit = 'hours';
|
||||
} else if (format === 'short') {
|
||||
if (diff < week) {
|
||||
unit = 'days';
|
||||
} else {
|
||||
return this.format('M/D/YY');
|
||||
}
|
||||
} else {
|
||||
return this.format('MMM D');
|
||||
}
|
||||
if (!(num && unit)) {
|
||||
num = moment.duration(diff)[unit]();
|
||||
}
|
||||
unitStr = unit = formats[unit][format];
|
||||
if (format === 'long' && num > 1) {
|
||||
unitStr += 's';
|
||||
}
|
||||
return num + unitStr;
|
||||
};
|
||||
moment.fn.twitterLong = function() {
|
||||
return twitterFormat.call(this, 'long');
|
||||
};
|
||||
moment.fn.twitter = moment.fn.twitterShort = function() {
|
||||
return twitterFormat.call(this, 'short');
|
||||
};
|
||||
return moment;
|
||||
};
|
||||
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('moment-twitter', ['moment'], function(moment) {
|
||||
return this.moment = initialize(moment);
|
||||
});
|
||||
} else if (typeof module !== 'undefined') {
|
||||
module.exports = initialize(require('moment'));
|
||||
} else if (typeof window !== "undefined" && window.moment) {
|
||||
this.moment = initialize(this.moment);
|
||||
}
|
||||
|
||||
}).call(this);
|
||||
@@ -377,7 +377,7 @@ $navbar-inverse-bg: #222 !default;
|
||||
$navbar-inverse-border: darken($navbar-inverse-bg, 10%) !default;
|
||||
|
||||
// Inverted navbar links
|
||||
$navbar-inverse-link-color: $gray-light !default;
|
||||
$navbar-inverse-link-color: lighten($gray-light, 20%) !default;
|
||||
$navbar-inverse-link-hover-color: #fff !default;
|
||||
$navbar-inverse-link-hover-bg: transparent !default;
|
||||
$navbar-inverse-link-active-color: $navbar-inverse-link-hover-color !default;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
@import "jq-ui-bootstrap/_menu";
|
||||
@import "jq-ui-bootstrap/_tooltip";
|
||||
|
||||
@import "compass/css3/animation";
|
||||
@import "compass/css3/transform";
|
||||
|
||||
body, .pad-top {
|
||||
padding-top: 50px;
|
||||
}
|
||||
@@ -17,7 +20,9 @@ body, .pad-top {
|
||||
|
||||
#userdropdown > li {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
|
||||
#userdropdown > li, #activity {
|
||||
.media-object {
|
||||
max-width: 3em;
|
||||
}
|
||||
@@ -63,3 +68,72 @@ textarea {
|
||||
.modal-dialog {
|
||||
z-index: inherit; // bug fix introduced in 52682ce
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
.default {
|
||||
background-color: $panel-default-heading-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-animation {
|
||||
position: relative;
|
||||
margin: 30px auto 0;
|
||||
|
||||
.circle {
|
||||
background-color: rgba(0,0,0,0);
|
||||
border: 5px solid rgba(0,183,229,0.9);
|
||||
opacity: .9;
|
||||
border-right: 5px solid rgba(0,0,0,0);
|
||||
border-left: 5px solid rgba(0,0,0,0);
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 0 35px #2187e7;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0 auto;
|
||||
@include animation(spinPulse 1s infinite ease-in-out);
|
||||
}
|
||||
|
||||
.circle1 {
|
||||
background-color: rgba(0,0,0,0);
|
||||
border: 5px solid rgba(0,183,229,0.9);
|
||||
opacity: .9;
|
||||
border-left: 5px solid rgba(0,0,0,0);
|
||||
border-right: 5px solid rgba(0,0,0,0);
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 0 15px #2187e7;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
top: -40px;
|
||||
@include animation(spinoffPulse 1s infinite linear);
|
||||
}
|
||||
|
||||
@include keyframes(spinPulse) {
|
||||
0% {
|
||||
@include rotate(160deg);
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 1px #2187e7;
|
||||
}
|
||||
|
||||
50% {
|
||||
@include rotate(145deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
@include rotate(-320deg);
|
||||
opacity: 0;
|
||||
};
|
||||
}
|
||||
|
||||
@include keyframes(spinoffPulse) {
|
||||
0% {
|
||||
@include rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
@include rotate(360deg);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
66
RIGS/templates/RIGS/activity_feed.html
Normal file
66
RIGS/templates/RIGS/activity_feed.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% load static %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script src="{% static "js/moment.min.js" %}"></script>
|
||||
<script src="{% static "js/moment-twitter.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
|
||||
// This keeps timeago values correct, but uses an insane amount of resources
|
||||
// $(function () {
|
||||
// setInterval(function() {
|
||||
// $('.date').each(function (index, dateElem) {
|
||||
// var $dateElem = $(dateElem);
|
||||
// var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
// $dateElem.text(formatted);
|
||||
// })
|
||||
// });
|
||||
// }, 10000);
|
||||
moment().twitter();
|
||||
|
||||
})
|
||||
$(document).ready(function() {
|
||||
$(function () {
|
||||
$( "#activity" ).hide();
|
||||
$( "#activity" ).load( "{% url 'activity_feed' %}", function() {
|
||||
$('#activity_loading').slideUp('slow',function(){
|
||||
$('#activity').slideDown('slow');
|
||||
});
|
||||
|
||||
$('#activity [data-toggle="popover"]').popover();
|
||||
|
||||
$('.date').each(function (index, dateElem) {
|
||||
var $dateElem = $(dateElem);
|
||||
var formatted = moment($dateElem.attr('data-date'),"DD/MM/YYYY HH:mm").twitterLong();
|
||||
$dateElem.text(formatted);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Recent Changes</h4>
|
||||
</div>
|
||||
|
||||
<div class="list-group">
|
||||
<div id="activity_loading" class="list-group-item loading-animation">
|
||||
<div class="circle"></div>
|
||||
<div class="circle1"></div>
|
||||
</div>
|
||||
<div id="activity">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
51
RIGS/templates/RIGS/activity_feed_data.html
Normal file
51
RIGS/templates/RIGS/activity_feed_data.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
{% load to_class_name from filters %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="list-group-item">
|
||||
<div class="media">
|
||||
{% for version in object_list %}
|
||||
|
||||
{% if not version.withPrevious %}
|
||||
{% if not forloop.first %}
|
||||
</div> {#/.media-body#}
|
||||
</div> {#/.media#}
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="media">
|
||||
{% endif %}
|
||||
<div class="media-left">
|
||||
{% if version.revision.user %}
|
||||
<a href="{% url 'profile_detail' pk=version.revision.user.pk %}" class="modal-href">
|
||||
<img class="media-object img-rounded" src="{{ version.revision.user.profile_picture}}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h5>{{ version.revision.user.name }}
|
||||
<span class="pull-right"><small><span class="date" data-date="{{version.revision.date_created}}"></span></small></span>
|
||||
</h5>
|
||||
|
||||
{% endif %}
|
||||
<p>
|
||||
<small>
|
||||
{% if version.old == None %}
|
||||
Created
|
||||
{% else %}
|
||||
Changed {% include 'RIGS/version_changes.html' %} in
|
||||
{% endif %}
|
||||
|
||||
{% include 'RIGS/object_button.html' with object=version.new %}
|
||||
</small>
|
||||
</p>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
88
RIGS/templates/RIGS/activity_table.html
Normal file
88
RIGS/templates/RIGS/activity_table.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
{% load to_class_name from filters %}
|
||||
|
||||
{% block title %}Rigboard Activity Stream{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script src="{% static "js/moment.min.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
|
||||
// This keeps timeago values correct, but uses an insane amount of resources
|
||||
// $(function () {
|
||||
// setInterval(function() {
|
||||
// $('.date').each(function (index, dateElem) {
|
||||
// var $dateElem = $(dateElem);
|
||||
// var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
// $dateElem.text(formatted);
|
||||
// })
|
||||
// });
|
||||
// }, 10000);
|
||||
|
||||
|
||||
$('.date').each(function (index, dateElem) {
|
||||
var $dateElem = $(dateElem);
|
||||
var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
$dateElem.text(formatted);
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3>Rigboard Activity Stream</h3>
|
||||
</div>
|
||||
<div class="text-right col-sm-12">{% paginator %}</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Object</td>
|
||||
<td>Version ID</td>
|
||||
<td>User</td>
|
||||
<td>Changes</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in object_list %}
|
||||
|
||||
<tr>
|
||||
<td>{{ version.revision.date_created }}</td>
|
||||
<td><a href="{{ version.new.get_absolute_url }}">{{version.new|to_class_name}} {{ version.new.pk|stringformat:"05d" }}</a></td>
|
||||
<td>{{ version.version.pk }}|{{ version.revision.pk }}</td>
|
||||
<td>{{ version.revision.user.name }}</td>
|
||||
<td>
|
||||
{% if version.old == None %}
|
||||
Object Created
|
||||
{% else %}
|
||||
{% include 'RIGS/version_changes.html' %}
|
||||
{% endif %} </td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="align-right">{% paginator %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -109,7 +109,15 @@
|
||||
|
||||
{% if event.is_rig %}
|
||||
<dt>Event MIC</dt>
|
||||
<dd>{{ event.mic.name }}</dd>
|
||||
<dd>
|
||||
{% if event.mic and perms.RIGS.view_profile %}
|
||||
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
|
||||
{{ event.mic.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ event.mic.name }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>Status</dt>
|
||||
@@ -119,18 +127,18 @@
|
||||
|
||||
{% if event.is_rig %}
|
||||
<dt>Crew Meet</dt>
|
||||
<dd>{{ event.meet_at|date:"d M Y H:i"|default:"" }}</dd>
|
||||
<dd>{{ event.meet_at|date:"D d M Y H:i"|default:"" }}</dd>
|
||||
<dd>{{ event.meet_info|default:"" }}</dd>
|
||||
|
||||
<dt>Access From</dt>
|
||||
<dd>{{ event.access_at|date:"d M Y H:i"|default:"" }}</dd>
|
||||
<dd>{{ event.access_at|date:"D d M Y H:i"|default:"" }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>Event Starts</dt>
|
||||
<dd>{{ event.start_date|date:"d M Y" }} {{ event.start_time|date:"H:i" }}</dd>
|
||||
<dd>{{ event.start_date|date:"D d M Y" }} {{ event.start_time|date:"H:i" }}</dd>
|
||||
|
||||
<dt>Event Ends</dt>
|
||||
<dd>{{ event.end_date|date:"d M Y" }} {{ event.end_time|date:"H:i" }}</dd>
|
||||
<dd>{{ event.end_date|date:"D d M Y" }} {{ event.end_time|date:"H:i" }}</dd>
|
||||
|
||||
<dd> </dd>
|
||||
|
||||
@@ -227,8 +235,10 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>Last edited at {{ object.last_edited_at|date:"SHORT_DATETIME_FORMAT" }}
|
||||
by {{ object.last_edited_by.name }}.
|
||||
<div>
|
||||
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -240,7 +250,9 @@
|
||||
{% block footer %}
|
||||
<div class="row">
|
||||
<div class="col-sm-10 align-left">
|
||||
Lasted edited at {{ object.last_edited_at|date:"SHORT_DATE_FORMAT" }} by {{ object.last_edited_by.name }}
|
||||
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="pull-right">
|
||||
|
||||
@@ -426,7 +426,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 text-right form-hws form-is_rig">
|
||||
<div class="col-sm-12 text-right form-hws form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}">
|
||||
<div class="btn-group btn-page">
|
||||
<button type="submit" class="btn btn-default" title="Save"><span
|
||||
class="glyphicon glyphicon-floppy-disk"></span>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<keepInFrame maxHeight="30">
|
||||
<para style="style.event_description">
|
||||
{{ object.description|default_if_none:""|linebreaks }}
|
||||
{{ object.description|default_if_none:""|linebreaksbr }}
|
||||
</para>
|
||||
</keepInFrame>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<h3>{{ object.venue.name }}</h3>
|
||||
{% if not invoice %}
|
||||
<keepInFrame>
|
||||
<para style="specific_description">{{ object.venue.address|default_if_none:""|linebreaks }}</para>
|
||||
<para style="specific_description">{{ object.venue.address|default_if_none:""|linebreaksbr }}</para>
|
||||
</keepInFrame>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -174,7 +174,7 @@
|
||||
{% if item.description %}
|
||||
</para>
|
||||
<para style="item_description">
|
||||
<em>{{ item.description|linebreaks }}</em>
|
||||
<em>{{ item.description|linebreaksbr }}</em>
|
||||
</para>
|
||||
<para>
|
||||
{% endif %}
|
||||
@@ -189,12 +189,18 @@
|
||||
<keepTogether>
|
||||
<blockTable style="totalTable" colWidths="300,115,80">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% if not invoice %}VAT Registration Number: 116252989{% endif %}</td>
|
||||
<td>Total (ex. VAT)</td>
|
||||
<td>£ {{ object.sum_total|floatformat:2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if not invoice %}VAT Registration Number: 116252989{% endif %}</td>
|
||||
<td>
|
||||
{% if not invoice %}
|
||||
<para>
|
||||
<b>The full hire fee is payable at least 10 days before the event.</b>
|
||||
</para>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
|
||||
<td>£ {{ object.vat|floatformat:2 }}</td>
|
||||
</tr>
|
||||
@@ -205,7 +211,7 @@
|
||||
{% if invoice %}
|
||||
VAT Registration Number: 116252989
|
||||
{% else %}
|
||||
<b>The full hire fee is payable at least 10 days before the event.</b>
|
||||
<b>This contract is not an invoice.</b>
|
||||
{% endif %}
|
||||
</para>
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
">
|
||||
<td class="hidden-xs">{{ event.pk }}</td>
|
||||
<td>
|
||||
<div><strong>{{ event.start_date|date:"SHORT_DATE_FORMAT" }}</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 %}
|
||||
<div><strong>{{ event.end_date|date:"SHORT_DATE_FORMAT" }}</strong></div>
|
||||
<div><strong>{{ event.end_date|date:"D d/m/Y" }}</strong></div>
|
||||
{% endif %}
|
||||
<span class="text-muted">{{ event.get_status_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<h4>
|
||||
<a href="{% url 'event_detail' event.pk %}">{{ event.name }}</a>
|
||||
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}>{{ event.name }}</a>
|
||||
{% if event.venue %}
|
||||
<small>at {{ event.venue }}</small>
|
||||
{% endif %}
|
||||
@@ -80,12 +80,18 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if event.mic or not event.is_rig %}
|
||||
{% if event.mic %}
|
||||
{{ event.mic.initials }}
|
||||
<div>
|
||||
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
|
||||
{% if perms.RIGS.view_profile %}
|
||||
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
|
||||
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
|
||||
</a>
|
||||
{% else %}
|
||||
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif event.is_rig %}
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -3,44 +3,76 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<h2>Rig Information Gathering System</h2>
|
||||
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="well">
|
||||
{% if user.is_authenticated %}
|
||||
<h3>Welcome back {{ user.get_full_name }}.<br />
|
||||
<small>Your rigboard initials are {{ user.initials }}</small></h3>
|
||||
{% endif %}
|
||||
<h3>There are currently {{ rig_count }} rigs coming up.</h3>
|
||||
<a class="btn btn-default" href="{% url 'rigboard' %}">View Rigboard</a>
|
||||
<a class="btn btn-default" href="{% url 'event_create' %}">
|
||||
New Event <span class="glyphicon glyphicon-plus"></span>
|
||||
</a>
|
||||
<div class="col-sm-12">
|
||||
<p><h4 class="list-group-item-heading" style="margin:0;">Welcome back {{ user.get_full_name }}, there are {{ rig_count }} rigs coming up.</h4>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-{% if perms.RIGS.view_event %}6{% else %}12{% endif %}">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="list-group-item-heading">Quick Links</h4>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<a class="list-group-item" href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> Rigboard</a>
|
||||
<a class="list-group-item" href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> Calendar</a>
|
||||
{% if perms.RIGS.add_event %}<a class="list-group-item" href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> New Event</a>{% endif %}
|
||||
|
||||
<div class="list-group-item default"></div>
|
||||
|
||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/forum" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
|
||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
|
||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="well">
|
||||
<div>
|
||||
<h4><a href="{% url 'person_list' %}">People</a></h4>
|
||||
<form class="form-inline" role="form" action="{% url 'person_list' %}" method="GET">
|
||||
<input type="search" name="q" class="form-control" placeholder="Search People" />
|
||||
<button type="submit" class="form-control"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</form>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Search Rigboard</h4>
|
||||
</div>
|
||||
<div>
|
||||
<h4><a href="{% url 'organisation_list' %}">Organisations</a></h4>
|
||||
<form class="form-inline" role="form" action="{% url 'organisation_list' %}" method="GET">
|
||||
<input type="search" name="q" class="form-control" placeholder="Search Organisations" />
|
||||
<button type="submit" class="form-control"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<h4><a href="{% url 'venue_list' %}">Venues</a></h4>
|
||||
<form class="form-inline" role="form" action="{% url 'venue_list' %}" method="GET">
|
||||
<input type="search" name="q" class="form-control" placeholder="Search Venues" />
|
||||
<button type="submit" class="form-control"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</form>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<form class="form" role="form" action="{% url 'person_list' %}" method="GET">
|
||||
<div class="input-group">
|
||||
<input type="search" name="q" class="form-control" placeholder="Search People" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<form class="form" role="form" action="{% url 'organisation_list' %}" method="GET">
|
||||
<div class="input-group">
|
||||
<input type="search" name="q" class="form-control" placeholder="Search Organisations" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<form class="form" role="form" action="{% url 'venue_list' %}" method="GET">
|
||||
<div class="input-group">
|
||||
<input type="search" name="q" class="form-control" placeholder="Search Venues" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.RIGS.view_event %}
|
||||
<div class="col-sm-6" >
|
||||
{% include 'RIGS/activity_feed.html' %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -44,8 +44,8 @@
|
||||
<tr id="new-item-row" class="item_row">
|
||||
<td>
|
||||
<span class="name"></span>
|
||||
<div class="description">
|
||||
<em></em>
|
||||
<div class="item-description">
|
||||
<em class="description"></em>
|
||||
</div>
|
||||
</td>
|
||||
<td>£ <span class="cost"></span></td>
|
||||
|
||||
4
RIGS/templates/RIGS/object_button.html
Normal file
4
RIGS/templates/RIGS/object_button.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% load to_class_name from filters %}
|
||||
{# pass in variable "object" to this template #}
|
||||
|
||||
<a title="{% if object.is_rig == False %}Non-rig{% elif object.dry_hire %}Dry Hire{% elif object.is_rig %}Rig{%else%}{{object|to_class_name}}{% endif %} | '{{object.name}}'" href="{{ object.get_absolute_url }}">{% if object.is_rig == False %}Non-rig{% elif object.dry_hire %}Dry Hire{% elif object.is_rig %}Rig{%else%}{{object|to_class_name}}{% endif %} | '{{object.name}}'</a>
|
||||
@@ -8,7 +8,9 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3>{{ object.name }}<br/>
|
||||
<span class="small">Last edited {{ object.last_edited_at }} by {{ object.last_edited_by.name }}</span>
|
||||
<span class="small"><a href="{% url 'organisation_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|date:"d/m/Y H:i" }} by {{ object.last_edited_by.name }}
|
||||
</a></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
@@ -67,8 +69,9 @@
|
||||
{% block footer %}
|
||||
<div class="row">
|
||||
<div class="col-sm-10 align-left">
|
||||
Lasted edited at {{ object.last_edited_at|date:"SHORT_DATE_FORMAT" }}
|
||||
by {{ object.last_edited_by.name }}
|
||||
<a href="{% url 'organisation_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="pull-right">
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
<h4>Details</h4>
|
||||
{% if not request.is_ajax %}
|
||||
<h3>{{ object.name }}<br/>
|
||||
<span class="small">Last edited {{ object.last_edited_at }} by {{ object.last_edited_by.name }}</span>
|
||||
<span class="small"><a href="{% url 'person_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|date:"d/m/Y H:i" }} by {{ object.last_edited_by.name }}
|
||||
</a></span>
|
||||
</h3>
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'person_update' object.pk %}" class="btn btn-primary">Edit <span
|
||||
@@ -59,7 +61,9 @@
|
||||
{% block footer %}
|
||||
<div class="row">
|
||||
<div class="col-sm-10 align-left">
|
||||
Lasted edited at {{ object.last_edited_at|date:"SHORT_DATE_FORMAT" }} by {{ object.last_edited_by.name }}
|
||||
<a href="{% url 'person_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|date:"d/m/Y H:i" }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="pull-right">
|
||||
|
||||
12
RIGS/templates/RIGS/profile_button.html
Normal file
12
RIGS/templates/RIGS/profile_button.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{# pass in variable "profile" to this template #}
|
||||
|
||||
<button title="{{profile.name}}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover focus' data-toggle="popover" data-content='
|
||||
<img src="{{profile.profile_picture}}" class="img-responsive img-rounded center-block" style="max-width:4em" />
|
||||
<dl class="dl-vertical">
|
||||
<dt>Email</dt>
|
||||
<dd>{{profile.email}}</dd>
|
||||
|
||||
<dt>Phone</dt>
|
||||
<dd>{{profile.phone}}</dd>
|
||||
</dl>
|
||||
'>{{profile.first_name}}</button>
|
||||
@@ -1,91 +1,104 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
|
||||
{% block title %}RIGS Profile {{object.pk}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
|
||||
<div class="col-sm-12">
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h3>{{object.name}}</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h3>{{object.name}}</h3>
|
||||
</div>
|
||||
{% if not request.is_ajax %}
|
||||
{% if object.pk == user.pk %}
|
||||
<div class="col-sm-6 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'profile_update_self' %}" class="btn btn-default">
|
||||
Edit Profile <span class="glyphicon glyphicon-pencil"></span>
|
||||
</a>
|
||||
<a href="{% url 'password_change' %}" class="btn btn-default">
|
||||
Change Password <span class="glyphicon glyphicon-lock"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="col-sm-8 ">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>First Name</dt>
|
||||
<dd>{{object.first_name}}</dd>
|
||||
|
||||
<dt>Last Name</dt>
|
||||
<dd>{{object.last_name}}</dd>
|
||||
|
||||
<dt>Email</dt>
|
||||
<dd>{{object.email}}</dd>
|
||||
|
||||
<dt>Last Login</dt>
|
||||
<dd>{{object.last_login|date:"d/m/Y H:i"}}</dd>
|
||||
|
||||
<dt>Date Joined</dt>
|
||||
<dd>{{object.date_joined|date:"d/m/Y H:i"}}</dd>
|
||||
|
||||
<dt>Initials</dt>
|
||||
<dd>{{object.initials}}</dd>
|
||||
|
||||
<dt>Phone</dt>
|
||||
<dd>{{object.phone}}</dd>
|
||||
</dl>
|
||||
{% if not request.is_ajax %}
|
||||
{% if object.pk == user.pk %}
|
||||
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'reset_api_key' %}" class="btn btn-default">
|
||||
{% if user.api_key %}Reset API Key{% else %}Generate API Key{% endif %}
|
||||
<span class="glyphicon glyphicon-repeat"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h4>Personal iCal Details</h4>
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>API Key</dt>
|
||||
<dd>
|
||||
{% if user.api_key %}
|
||||
{{user.api_key}}
|
||||
{% else %}
|
||||
No API Key Generated
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Calendar URL</dt>
|
||||
<dd>
|
||||
{% if user.api_key %}
|
||||
<pre>http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}</pre>
|
||||
<small><a href="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 %}">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>
|
||||
{% else %}
|
||||
<pre>No API Key Generated</pre>
|
||||
{% endif %}
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="center-block">
|
||||
<img src="{{object.profile_picture}}" class="img-responsive img-rounded" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% if object.pk == user.pk %}
|
||||
<div class="col-sm-6 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'profile_update_self' %}" class="btn btn-default">
|
||||
Edit Profile <span class="glyphicon glyphicon-pencil"></span>
|
||||
</a>
|
||||
<a href="{% url 'password_change' %}" class="btn btn-default">
|
||||
Change Password <span class="glyphicon glyphicon-lock"></span>
|
||||
</a>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h4>Events</h4>
|
||||
{% with object.latest_events as events %}
|
||||
{% include 'RIGS/event_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-8 ">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>First Name</dt>
|
||||
<dd>{{object.first_name}}</dd>
|
||||
|
||||
<dt>Last Name</dt>
|
||||
<dd>{{object.last_name}}</dd>
|
||||
|
||||
<dt>Email</dt>
|
||||
<dd>{{object.email}}</dd>
|
||||
|
||||
<dt>Last Login</dt>
|
||||
<dd>{{object.last_login|date:"d/m/Y H:i"}}</dd>
|
||||
|
||||
<dt>Date Joined</dt>
|
||||
<dd>{{object.date_joined|date:"d/m/Y H:i"}}</dd>
|
||||
|
||||
<dt>Initials</dt>
|
||||
<dd>{{object.initials}}</dd>
|
||||
|
||||
<dt>Phone</dt>
|
||||
<dd>{{object.phone}}</dd>
|
||||
</dl>
|
||||
{% if object.pk == user.pk %}
|
||||
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'reset_api_key' %}" class="btn btn-default">
|
||||
{% if user.api_key %}Reset API Key{% else %}Generate API Key{% endif %}
|
||||
<span class="glyphicon glyphicon-repeat"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h4>Personal iCal Details</h4>
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>API Key</dt>
|
||||
<dd>
|
||||
{% if user.api_key %}
|
||||
{{user.api_key}}
|
||||
{% else %}
|
||||
No API Key Generated
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Calendar URL</dt>
|
||||
<dd>
|
||||
{% if user.api_key %}
|
||||
<pre>http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}</pre>
|
||||
<small><a href="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 %}">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 "PyRIGS Calendar".</small>
|
||||
{% else %}
|
||||
<pre>No API Key Generated</pre>
|
||||
{% endif %}
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="center-block">
|
||||
<img src="{{object.profile_picture}}" class="img-responsive img-rounded" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -8,11 +8,14 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-10">
|
||||
<h3>Rigboard</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.RIGS.add_event %}
|
||||
<div class="col-sm-2">
|
||||
<a href="{% url 'event_create' %}" class="btn btn-default pull-right">New <span
|
||||
class="glyphicon glyphicon-plus"></span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% comment %}
|
||||
{# Bring search back at a later date #}
|
||||
<div class="col-sm-3 col-sm-offset-9">
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<div class="col-sm-10 col-sm-offset-1">
|
||||
{% if not request.is_ajax %}
|
||||
<h3>{{ object.name }}<br/>
|
||||
<span class="small">Last edited {{ object.last_edited_at }} by {{ object.last_edited_by }}</span>
|
||||
<span class="small"><a href="{% url 'venue_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|date:"d/m/Y H:i" }} by {{ object.last_edited_by.name }}
|
||||
</a></span>
|
||||
</h3>
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'venue_update' object.pk %}" class="btn btn-primary">Edit <span
|
||||
@@ -49,7 +51,9 @@
|
||||
{% block footer %}
|
||||
<div class="row">
|
||||
<div class="col-sm-10 align-left">
|
||||
Lasted edited at {{ object.last_edited_at|date:"SHORT_DATE_FORMAT" }} by {{ object.last_edited_by }}
|
||||
<a href="{% url 'venue_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|date:"d/m/Y H:i" }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="pull-right">
|
||||
|
||||
23
RIGS/templates/RIGS/version_changes.html
Normal file
23
RIGS/templates/RIGS/version_changes.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% 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='
|
||||
|
||||
{% if change.new %}<div class="alert alert-success {% if change.long %}overflow-ellipsis{% endif %}">{{change.new|linebreaksbr}}</div>{% endif %}
|
||||
{% if change.old %}<div class="alert alert-danger {% if change.long %}overflow-ellipsis{% endif %}">{{change.old|linebreaksbr}}</div>{% endif %}
|
||||
|
||||
'>{{ change.field.verbose_name }}</button>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% 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='
|
||||
{% for change in itemChange.changes %}
|
||||
<h4>{{ change.field.verbose_name }}</h4>
|
||||
|
||||
{% if change.new %}<div class="alert alert-success">{{change.new|linebreaksbr}}</div>{% endif %}
|
||||
{% if change.old %}<div class="alert alert-danger">{{change.old|linebreaksbr}}</div>{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
'>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button>
|
||||
{% endfor %}
|
||||
63
RIGS/templates/RIGS/version_history.html
Normal file
63
RIGS/templates/RIGS/version_history.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% load to_class_name from filters %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{object|to_class_name}} {{ object.pk|stringformat:"05d" }} - Revision History{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3><a href="{{ object.get_absolute_url }}">{{object|to_class_name}} {{ object.pk|stringformat:"05d" }}</a> - Revision History</h3>
|
||||
</div>
|
||||
<div class="text-right col-sm-12">{% paginator %}</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Version ID</td>
|
||||
<td>User</td>
|
||||
<td>Changes</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in object_list %}
|
||||
{% if version.item_changes or version.field_changes or version.old == None %}
|
||||
<tr>
|
||||
<td>{{ version.revision.date_created }}</td>
|
||||
<td>{{ version.version.pk }}|{{ version.revision.pk }}</td>
|
||||
<td>{{ version.revision.user.name }}</td>
|
||||
<td>
|
||||
{% if version.old == None %}
|
||||
Object Created
|
||||
{% else %}
|
||||
{% include 'RIGS/version_changes.html' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="align-right">{% paginator %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
from django import template
|
||||
from django import forms
|
||||
from django.forms.forms import NON_FIELD_ERRORS
|
||||
from django.forms.util import ErrorDict
|
||||
from django.forms.utils import ErrorDict
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -9,6 +9,10 @@ register = template.Library()
|
||||
def multiply(value, arg):
|
||||
return value*arg
|
||||
|
||||
@register.filter
|
||||
def to_class_name(value):
|
||||
return value.__class__.__name__
|
||||
|
||||
@register.filter
|
||||
def nice_errors(form, non_field_msg='General form errors'):
|
||||
nice_errors = ErrorDict()
|
||||
|
||||
471
RIGS/test_functional.py
Normal file
471
RIGS/test_functional.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import LiveServerTestCase
|
||||
from django.core import mail
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from RIGS import models
|
||||
import re
|
||||
import os
|
||||
from django.db import transaction
|
||||
import reversion
|
||||
import json
|
||||
|
||||
|
||||
class UserRegistrationTest(LiveServerTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.browser = webdriver.Firefox()
|
||||
os.environ['RECAPTCHA_TESTING'] = 'True'
|
||||
|
||||
def tearDown(self):
|
||||
self.browser.quit()
|
||||
os.environ['RECAPTCHA_TESTING'] = 'False'
|
||||
|
||||
def test_registration(self):
|
||||
# Navigate to the registration page
|
||||
self.browser.get(self.live_server_url + '/user/register/')
|
||||
# self.browser.implicitly_wait(3)
|
||||
title_text = self.browser.find_element_by_tag_name('h3').text
|
||||
self.assertIn("User Registration", title_text)
|
||||
|
||||
# Check the form invites correctly
|
||||
username = self.browser.find_element_by_id('id_username')
|
||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||
email = self.browser.find_element_by_id('id_email')
|
||||
self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
|
||||
# If this is correct we don't need to test it later
|
||||
self.assertEqual(email.get_attribute('type'), 'email')
|
||||
password1 = self.browser.find_element_by_id('id_password1')
|
||||
self.assertEqual(password1.get_attribute('placeholder'), 'Password')
|
||||
self.assertEqual(password1.get_attribute('type'), 'password')
|
||||
password2 = self.browser.find_element_by_id('id_password2')
|
||||
self.assertEqual(
|
||||
password2.get_attribute('placeholder'), 'Password confirmation')
|
||||
self.assertEqual(password2.get_attribute('type'), 'password')
|
||||
first_name = self.browser.find_element_by_id('id_first_name')
|
||||
self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
|
||||
last_name = self.browser.find_element_by_id('id_last_name')
|
||||
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
||||
initials = self.browser.find_element_by_id('id_initials')
|
||||
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
||||
phone = self.browser.find_element_by_id('id_phone')
|
||||
self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||
|
||||
# Fill the form out incorrectly
|
||||
username.send_keys('TestUsername')
|
||||
email.send_keys('test@example.com')
|
||||
password1.send_keys('correcthorsebatterystaple')
|
||||
# deliberate mistake
|
||||
password2.send_keys('correcthorsebatterystapleerror')
|
||||
first_name.send_keys('John')
|
||||
last_name.send_keys('Smith')
|
||||
initials.send_keys('JS')
|
||||
phone.send_keys('0123456789')
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
|
||||
# Submit incorrect form
|
||||
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
||||
submit.click()
|
||||
# Restablish error fields
|
||||
password1 = self.browser.find_element_by_id('id_password1')
|
||||
password2 = self.browser.find_element_by_id('id_password2')
|
||||
|
||||
# Read what the error is
|
||||
alert = self.browser.find_element_by_css_selector(
|
||||
'div.alert-danger').text
|
||||
self.assertIn("password fields didn't match", alert)
|
||||
|
||||
# Passwords should be empty
|
||||
self.assertEqual(password1.get_attribute('value'), '')
|
||||
self.assertEqual(password2.get_attribute('value'), '')
|
||||
|
||||
# Correct error
|
||||
password1.send_keys('correcthorsebatterystaple')
|
||||
password2.send_keys('correcthorsebatterystaple')
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
|
||||
# Submit again
|
||||
password2.send_keys(Keys.ENTER)
|
||||
|
||||
# Check we have a success message
|
||||
alert = self.browser.find_element_by_css_selector(
|
||||
'div.alert-success').text
|
||||
self.assertIn('register', alert)
|
||||
self.assertIn('email', alert)
|
||||
|
||||
# Check Email
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
email = mail.outbox[0]
|
||||
self.assertIn('activation required', email.subject)
|
||||
urls = re.findall(
|
||||
'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
|
||||
self.assertEqual(len(urls), 1)
|
||||
|
||||
mail.outbox = [] # empty this for later
|
||||
|
||||
# Follow link
|
||||
self.browser.get(urls[0]) # go to the first link
|
||||
|
||||
# Complete registration
|
||||
title_text = self.browser.find_element_by_tag_name('h2').text
|
||||
self.assertIn('Complete', title_text)
|
||||
|
||||
# Test login
|
||||
self.browser.get(self.live_server_url + '/user/login')
|
||||
username = self.browser.find_element_by_id('id_username')
|
||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||
password = self.browser.find_element_by_id('id_password')
|
||||
self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
||||
self.assertEqual(password.get_attribute('type'), 'password')
|
||||
|
||||
username.send_keys('TestUsername')
|
||||
password.send_keys('correcthorsebatterystaple')
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
password.send_keys(Keys.ENTER)
|
||||
|
||||
# Check we are logged in
|
||||
udd = self.browser.find_element_by_class_name('navbar').text
|
||||
self.assertIn('Hi John', udd)
|
||||
|
||||
# All is well
|
||||
|
||||
|
||||
class EventTest(LiveServerTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.profile = models.Profile(
|
||||
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
|
||||
self.profile.set_password("EventTestPassword")
|
||||
self.profile.save()
|
||||
|
||||
self.browser = webdriver.Firefox()
|
||||
os.environ['RECAPTCHA_TESTING'] = 'True'
|
||||
|
||||
def tearDown(self):
|
||||
self.browser.quit()
|
||||
os.environ['RECAPTCHA_TESTING'] = 'False'
|
||||
|
||||
def authenticate(self, n=None):
|
||||
self.assertIn(
|
||||
self.live_server_url + '/user/login/', self.browser.current_url)
|
||||
if n:
|
||||
self.assertIn('?next=%s' % n, self.browser.current_url)
|
||||
username = self.browser.find_element_by_id('id_username')
|
||||
password = self.browser.find_element_by_id('id_password')
|
||||
submit = self.browser.find_element_by_css_selector(
|
||||
'input[type=submit]')
|
||||
|
||||
username.send_keys("EventTest")
|
||||
password.send_keys("EventTestPassword")
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
submit.click()
|
||||
|
||||
self.assertEqual(self.live_server_url + n, self.browser.current_url)
|
||||
|
||||
def testRigboardButtons(self):
|
||||
# Requests address
|
||||
self.browser.get(self.live_server_url + '/rigboard/')
|
||||
# Gets redirected to login
|
||||
self.authenticate('/rigboard/')
|
||||
|
||||
# Completes and comes back to rigboard
|
||||
# Clicks add new
|
||||
self.browser.find_element_by_partial_link_text("New").click()
|
||||
self.assertEqual(
|
||||
self.live_server_url + '/event/create/', self.browser.current_url)
|
||||
self.browser.get(self.live_server_url + '/rigboard/')
|
||||
|
||||
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)
|
||||
self.browser.implicitly_wait(3) #Set session-long wait (only works for non-existant DOM objects)
|
||||
|
||||
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("TBD%", self.browser.find_element_by_id('vat-rate').text)
|
||||
self.assertEqual("0.00", self.browser.find_element_by_id('vat').text)
|
||||
self.assertEqual("47.90", 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)
|
||||
self.browser.implicitly_wait(3)
|
||||
|
||||
# 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 testEventDetail(self):
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
person = models.Person(name="Event Detail Person", email="eventdetail@person.tests.rigs", phone="123 123")
|
||||
person.save()
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
organisation = models.Organisation(name="Event Detail Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
venue = models.Venue(name="Event Detail Venue").save()
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
event = models.Event(
|
||||
name="Detail Test",
|
||||
description="This is an event to test the detail view",
|
||||
notes="It is going to be aweful",
|
||||
person=person,
|
||||
organisation=organisation,
|
||||
start_date='2015-06-04'
|
||||
)
|
||||
event.save()
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
item1 = models.EventItem(
|
||||
event=event,
|
||||
name="Detail Item 1",
|
||||
cost="10.00",
|
||||
quantity="1",
|
||||
order=1
|
||||
).save()
|
||||
item2 = models.EventItem(
|
||||
event=event,
|
||||
name="Detail Item 2",
|
||||
description="Foo",
|
||||
cost="9.72",
|
||||
quantity="3",
|
||||
order=2,
|
||||
).save()
|
||||
|
||||
|
||||
self.browser.get(self.live_server_url + '/event/%d'%event.pk)
|
||||
self.authenticate('/event/%d/'%event.pk)
|
||||
self.assertIn("N%05d | %s"%(event.pk, event.name), self.browser.find_element_by_xpath('//h1').text)
|
||||
|
||||
personPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
|
||||
self.assertEqual(person.name, personPanel.find_element_by_xpath('//dt[text()="Person"]/following-sibling::dd[1]').text)
|
||||
self.assertEqual(person.email, personPanel.find_element_by_xpath('//dt[text()="Email"]/following-sibling::dd[1]').text)
|
||||
self.assertEqual(person.phone, personPanel.find_element_by_xpath('//dt[text()="Phone Number"]/following-sibling::dd[1]').text)
|
||||
|
||||
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
|
||||
|
||||
|
||||
class animation_is_finished(object):
|
||||
""" Checks if animation is done """
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __call__(self, driver):
|
||||
numberAnimating = driver.execute_script('return $(":animated").length')
|
||||
finished = numberAnimating == 0
|
||||
return finished
|
||||
233
RIGS/test_models.py
Normal file
233
RIGS/test_models.py
Normal file
@@ -0,0 +1,233 @@
|
||||
from django.test import TestCase
|
||||
from RIGS import models
|
||||
from datetime import date, timedelta
|
||||
from decimal import *
|
||||
|
||||
class VatRateTestCase(TestCase):
|
||||
def setUp(self):
|
||||
models.VatRate.objects.create(start_at='2014-03-01',rate=0.20,comment='test1')
|
||||
models.VatRate.objects.create(start_at='2016-03-01',rate=0.15,comment='test2')
|
||||
|
||||
def test_find_correct(self):
|
||||
r = models.VatRate.objects.find_rate('2015-03-01')
|
||||
self.assertEqual(r.comment, 'test1')
|
||||
r = models.VatRate.objects.find_rate('2016-03-01')
|
||||
self.assertEqual(r.comment, 'test2')
|
||||
|
||||
def test_percent_correct(self):
|
||||
r = models.VatRate.objects.get(rate=0.20)
|
||||
self.assertEqual(r.as_percent, 20)
|
||||
|
||||
class EventTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.all_events = set(range(1, 18))
|
||||
self.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
|
||||
self.not_current_events = set(self.all_events) - set(self.current_events)
|
||||
|
||||
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
|
||||
self.profile = models.Profile.objects.create(username="testuser1", email="1@test.com")
|
||||
|
||||
# produce 7 normal events - 5 current
|
||||
models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6), description="start future no end")
|
||||
models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end")
|
||||
models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(), description="start today with end today")
|
||||
models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end")
|
||||
models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21', description="start past with end past")
|
||||
models.Event.objects.create(name="TE E6", start_date=date.today()-timedelta(days=2), end_date=date.today()+timedelta(days=2), description="start past, end future")
|
||||
models.Event.objects.create(name="TE E7", start_date=date.today()+timedelta(days=2), end_date=date.today()+timedelta(days=2), description="start + end in future")
|
||||
|
||||
# 2 cancelled - 1 current
|
||||
models.Event.objects.create(name="TE E8", start_date=date.today()+timedelta(days=2), end_date=date.today()+timedelta(days=2), status=models.Event.CANCELLED, description="cancelled in future")
|
||||
models.Event.objects.create(name="TE E9", start_date=date.today()-timedelta(days=1), end_date=date.today()+timedelta(days=2), status=models.Event.CANCELLED, description="cancelled and started")
|
||||
|
||||
# 5 dry hire - 3 current
|
||||
models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today")
|
||||
models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile, description="dryhire today, checked in")
|
||||
models.Event.objects.create(name="TE E12", start_date=date.today()-timedelta(days=1), dry_hire=True, status=models.Event.BOOKED, description="dryhire past")
|
||||
models.Event.objects.create(name="TE E13", start_date=date.today()-timedelta(days=2), dry_hire=True, checked_in_by=self.profile, description="dryhire past checked in")
|
||||
models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, status=models.Event.CANCELLED, description="dryhire today cancelled")
|
||||
|
||||
# 4 non rig - 3 current
|
||||
models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, description="non rig today")
|
||||
models.Event.objects.create(name="TE E16", start_date=date.today()+timedelta(days=1), is_rig=False, description="non rig tomorrow")
|
||||
models.Event.objects.create(name="TE E17", start_date=date.today()-timedelta(days=1), is_rig=False, description="non rig yesterday")
|
||||
models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED, description="non rig today cancelled")
|
||||
|
||||
def test_count(self):
|
||||
# Santiy check we have the expected events created
|
||||
self.assertEqual(models.Event.objects.count(), 18, "Incorrect number of events, check setup")
|
||||
|
||||
def test_rig_count(self):
|
||||
# by my count this is 7
|
||||
self.assertEqual(models.Event.objects.rig_count(), 8)
|
||||
|
||||
def test_current_events(self):
|
||||
current_events = models.Event.objects.current_events()
|
||||
self.assertEqual(len(current_events), len(self.current_events))
|
||||
for eid in self.current_events:
|
||||
self.assertIn(models.Event.objects.get(name="TE E%d"%eid), current_events)
|
||||
|
||||
for eid in self.not_current_events:
|
||||
self.assertNotIn(models.Event.objects.get(name="TE E%d"%eid), current_events)
|
||||
|
||||
def test_related_venue(self):
|
||||
v1 = models.Venue.objects.create(name="TE V1")
|
||||
v2 = models.Venue.objects.create(name="TE V2")
|
||||
events = models.Event.objects.all()
|
||||
for event in events[:2]:
|
||||
event.venue = v1
|
||||
event.save()
|
||||
for event in events[3:4]:
|
||||
event.venue = v2
|
||||
event.save()
|
||||
|
||||
events = models.Event.objects.all()
|
||||
self.assertItemsEqual(events[:2], v1.latest_events)
|
||||
self.assertItemsEqual(events[3:4], v2.latest_events)
|
||||
|
||||
def test_related_vatrate(self):
|
||||
self.assertEqual(self.vatrate, models.Event.objects.all()[0].vat_rate)
|
||||
|
||||
def test_related_person(self):
|
||||
p1 = models.Person.objects.create(name="TE P1")
|
||||
p2 = models.Person.objects.create(name="TE P2")
|
||||
|
||||
events = models.Event.objects.all()
|
||||
for event in events[:2]:
|
||||
event.person = p1
|
||||
event.save()
|
||||
for event in events[3:4]:
|
||||
event.person = p2
|
||||
event.save()
|
||||
|
||||
events = models.Event.objects.all()
|
||||
self.assertItemsEqual(events[:2], p1.latest_events)
|
||||
self.assertItemsEqual(events[3:4], p2.latest_events)
|
||||
|
||||
def test_related_organisation(self):
|
||||
o1 = models.Organisation.objects.create(name="TE O1")
|
||||
o2 = models.Organisation.objects.create(name="TE O2")
|
||||
|
||||
events = models.Event.objects.all()
|
||||
for event in events[:2]:
|
||||
event.organisation = o1
|
||||
event.save()
|
||||
for event in events[3:4]:
|
||||
event.organisation = o2
|
||||
event.save()
|
||||
|
||||
events = models.Event.objects.all()
|
||||
self.assertItemsEqual(events[:2], o1.latest_events)
|
||||
self.assertItemsEqual(events[3:4], o2.latest_events)
|
||||
|
||||
def test_organisation_person_join(self):
|
||||
p1 = models.Person.objects.create(name="TE P1")
|
||||
p2 = models.Person.objects.create(name="TE P2")
|
||||
o1 = models.Organisation.objects.create(name="TE O1")
|
||||
o2 = models.Organisation.objects.create(name="TE O2")
|
||||
|
||||
events = models.Event.objects.all()
|
||||
# p1 in o1 + o2, p2 in o1
|
||||
for event in events[:2]:
|
||||
event.person = p1
|
||||
event.organisation = o1
|
||||
event.save()
|
||||
for event in events[3:4]:
|
||||
event.person = p1
|
||||
event.organisation = o2
|
||||
event.save()
|
||||
for event in events[5:7]:
|
||||
event.person = p2
|
||||
event.organisation = o1
|
||||
event.save()
|
||||
|
||||
events = models.Event.objects.all()
|
||||
|
||||
# Check person's organisations
|
||||
self.assertIn(o1, p1.organisations)
|
||||
self.assertIn(o2, p1.organisations)
|
||||
self.assertIn(o1, p2.organisations)
|
||||
self.assertNotIn(o2, p2.organisations)
|
||||
|
||||
# Check organisation's persons
|
||||
self.assertIn(p1, o1.persons)
|
||||
self.assertIn(p2, o1.persons)
|
||||
self.assertIn(p1, o2.persons)
|
||||
self.assertNotIn(p2, o2.persons)
|
||||
|
||||
def test_cancelled_property(self):
|
||||
event = models.Event.objects.all()[0]
|
||||
event.status = models.Event.CANCELLED
|
||||
event.save()
|
||||
event = models.Event.objects.all()[0]
|
||||
self.assertEqual(event.status, models.Event.CANCELLED)
|
||||
self.assertTrue(event.cancelled)
|
||||
event.status = models.Event.PROVISIONAL
|
||||
event.save()
|
||||
|
||||
def test_confirmed_property(self):
|
||||
event = models.Event.objects.all()[0]
|
||||
event.status = models.Event.CONFIRMED
|
||||
event.save()
|
||||
event = models.Event.objects.all()[0]
|
||||
self.assertEqual(event.status, models.Event.CONFIRMED)
|
||||
self.assertTrue(event.confirmed)
|
||||
event.status = models.Event.PROVISIONAL
|
||||
event.save()
|
||||
|
||||
class EventItemTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.e1 = models.Event.objects.create(name="TI E1", start_date=date.today())
|
||||
self.e2 = models.Event.objects.create(name="TI E2", start_date=date.today())
|
||||
|
||||
def test_item_cost(self):
|
||||
item = models.EventItem.objects.create(event=self.e1, name="TI I1", quantity=1, cost=1.00, order=1)
|
||||
self.assertEqual(item.total_cost, 1.00)
|
||||
|
||||
item.cost = 2.50
|
||||
self.assertEqual(item.total_cost, 2.50)
|
||||
|
||||
item.quantity = 4
|
||||
self.assertEqual(item.total_cost, 10.00)
|
||||
|
||||
# need to tidy up
|
||||
item.delete()
|
||||
|
||||
def test_item_order(self):
|
||||
i1 = models.EventItem.objects.create(event=self.e1, name="TI I1", quantity=1, cost=1.00, order=1)
|
||||
i2 = models.EventItem.objects.create(event=self.e1, name="TI I2", quantity=1, cost=1.00, order=2)
|
||||
|
||||
items = self.e1.items.all()
|
||||
self.assertListEqual([i1, i2], list(items))
|
||||
|
||||
class EventPricingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
|
||||
models.VatRate.objects.create(rate=0.10, comment="TP V2", start_at=date.today()-timedelta(days=1))
|
||||
self.e1 = models.Event.objects.create(name="TP E1", start_date=date.today()-timedelta(days=2))
|
||||
self.e2 = models.Event.objects.create(name="TP E2", start_date=date.today())
|
||||
|
||||
# Create some items E1, total 70.40
|
||||
# Create some items E2, total 381.20
|
||||
self.i1 = models.EventItem.objects.create(event=self.e1, name="TP I1", quantity=1, cost=50.00, order=1)
|
||||
self.i2 = models.EventItem.objects.create(event=self.e1, name="TP I2", quantity=2, cost=3.20, order=2)
|
||||
self.i3 = models.EventItem.objects.create(event=self.e1, name="TP I3", quantity=7, cost=2.00, order=3)
|
||||
self.i4 = models.EventItem.objects.create(event=self.e2, name="TP I4", quantity=2, cost=190.60, order=1)
|
||||
|
||||
# Decimal type is needed here as that is what is returned from the model.
|
||||
# Using anything else results in a failure due to floating point arritmetic
|
||||
def test_sum_totals(self):
|
||||
self.assertEqual(self.e1.sum_total, Decimal('70.40'))
|
||||
self.assertEqual(self.e2.sum_total, Decimal('381.20'))
|
||||
|
||||
def test_vat_rate(self):
|
||||
self.assertEqual(self.e1.vat_rate.rate, Decimal('0.20'))
|
||||
self.assertEqual(self.e2.vat_rate.rate, Decimal('0.10'))
|
||||
|
||||
def test_vat_ammount(self):
|
||||
self.assertEqual(self.e1.vat, Decimal('14.08'))
|
||||
self.assertEqual(self.e2.vat, Decimal('38.12'))
|
||||
|
||||
def test_grand_total(self):
|
||||
self.assertEqual(self.e1.total, Decimal('84.48'))
|
||||
self.assertEqual(self.e2.total, Decimal('419.32'))
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
32
RIGS/urls.py
32
RIGS/urls.py
@@ -1,6 +1,6 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from RIGS import views, rigboard, finance, ical, forms
|
||||
from RIGS import models, views, rigboard, finance, ical, versioning, forms
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from PyRIGS.decorators import permission_required_with_403
|
||||
@@ -10,7 +10,7 @@ urlpatterns = patterns('',
|
||||
# Examples:
|
||||
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
||||
# url(r'^blog/', include('blog.urls')),
|
||||
url('^$', views.Index.as_view(), name='index'),
|
||||
url('^$', login_required(views.Index.as_view()), name='index'),
|
||||
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
||||
|
||||
url('^user/login/$', 'RIGS.views.login', name='login'),
|
||||
@@ -25,6 +25,9 @@ urlpatterns = patterns('',
|
||||
url(r'^people/(?P<pk>\d+)/$',
|
||||
permission_required_with_403('RIGS.view_person')(views.PersonDetail.as_view()),
|
||||
name='person_detail'),
|
||||
url(r'^people/(?P<pk>\d+)/history/$',
|
||||
permission_required_with_403('RIGS.view_person')(versioning.VersionHistory.as_view()),
|
||||
name='person_history', kwargs={'model': models.Person}),
|
||||
url(r'^people/(?P<pk>\d+)/edit/$',
|
||||
permission_required_with_403('RIGS.change_person')(views.PersonUpdate.as_view()),
|
||||
name='person_update'),
|
||||
@@ -39,6 +42,9 @@ urlpatterns = patterns('',
|
||||
url(r'^organisations/(?P<pk>\d+)/$',
|
||||
permission_required_with_403('RIGS.view_organisation')(views.OrganisationDetail.as_view()),
|
||||
name='organisation_detail'),
|
||||
url(r'^organisations/(?P<pk>\d+)/history/$',
|
||||
permission_required_with_403('RIGS.view_organisation')(versioning.VersionHistory.as_view()),
|
||||
name='organisation_history', kwargs={'model': models.Organisation}),
|
||||
url(r'^organisations/(?P<pk>\d+)/edit/$',
|
||||
permission_required_with_403('RIGS.change_organisation')(views.OrganisationUpdate.as_view()),
|
||||
name='organisation_update'),
|
||||
@@ -53,6 +59,9 @@ urlpatterns = patterns('',
|
||||
url(r'^venues/(?P<pk>\d+)/$',
|
||||
permission_required_with_403('RIGS.view_venue')(views.VenueDetail.as_view()),
|
||||
name='venue_detail'),
|
||||
url(r'^venues/(?P<pk>\d+)/history/$',
|
||||
permission_required_with_403('RIGS.view_venue')(versioning.VersionHistory.as_view()),
|
||||
name='venue_history', kwargs={'model': models.Venue}),
|
||||
url(r'^venues/(?P<pk>\d+)/edit/$',
|
||||
permission_required_with_403('RIGS.change_venue')(views.VenueUpdate.as_view()),
|
||||
name='venue_update'),
|
||||
@@ -60,7 +69,13 @@ urlpatterns = patterns('',
|
||||
# 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/archive/$', RedirectView.as_view(pattern_name='event_archive')),
|
||||
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')),
|
||||
url(r'^rigboard/activity/$',
|
||||
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),
|
||||
name='activity_table'),
|
||||
url(r'^rigboard/activity/feed/$',
|
||||
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
|
||||
name='activity_feed'),
|
||||
|
||||
url(r'^event/(?P<pk>\d+)/$',
|
||||
permission_required_with_403('RIGS.view_event')(rigboard.EventDetail.as_view()),
|
||||
@@ -80,6 +95,11 @@ urlpatterns = patterns('',
|
||||
url(r'^event/archive/$', login_required()(rigboard.EventArchive.as_view()),
|
||||
name='event_archive'),
|
||||
|
||||
url(r'^event/(?P<pk>\d+)/history/$',
|
||||
permission_required_with_403('RIGS.view_event')(versioning.VersionHistory.as_view()),
|
||||
name='event_history', kwargs={'model': models.Event}),
|
||||
|
||||
|
||||
|
||||
# Finance
|
||||
url(r'^invoice/$',
|
||||
@@ -129,8 +149,8 @@ urlpatterns = patterns('',
|
||||
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', (views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||
|
||||
# Legacy URL's
|
||||
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(pattern_name='event_detail')),
|
||||
url(r'^bookings/$', RedirectView.as_view(pattern_name='rigboard')),
|
||||
url(r'^bookings/past/$', RedirectView.as_view(pattern_name='event_archive')),
|
||||
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(permanent=True,pattern_name='event_detail')),
|
||||
url(r'^bookings/$', RedirectView.as_view(permanent=True,pattern_name='rigboard')),
|
||||
url(r'^bookings/past/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')),
|
||||
)
|
||||
|
||||
|
||||
271
RIGS/versioning.py
Normal file
271
RIGS/versioning.py
Normal file
@@ -0,0 +1,271 @@
|
||||
import logging
|
||||
from django.views import generic
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
# Versioning
|
||||
import reversion
|
||||
import simplejson
|
||||
from reversion.models import Version
|
||||
from django.contrib.contenttypes.models import ContentType # Used to lookup the content_type
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.db.models import ForeignKey, IntegerField, EmailField
|
||||
|
||||
from RIGS import models, forms
|
||||
import datetime
|
||||
import re
|
||||
|
||||
logger = logging.getLogger('tec.pyrigs')
|
||||
|
||||
|
||||
def model_compare(oldObj, newObj, excluded_keys=[]):
|
||||
# recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects
|
||||
try:
|
||||
theFields = oldObj._meta.fields #This becomes deprecated in Django 1.8!!!!!!!!!!!!! (but an alternative becomes available)
|
||||
except AttributeError:
|
||||
theFields = newObj._meta.fields
|
||||
|
||||
|
||||
class FieldCompare(object):
|
||||
def __init__(self, field=None, old=None, new=None):
|
||||
self.field = field
|
||||
self._old = old
|
||||
self._new = new
|
||||
|
||||
def display_value(self, value):
|
||||
if isinstance(self.field, IntegerField) and len(self.field.choices) > 0:
|
||||
return [x[1] for x in self.field.choices if x[0] == value][0]
|
||||
return value
|
||||
|
||||
@property
|
||||
def old(self):
|
||||
return self.display_value(self._old)
|
||||
|
||||
@property
|
||||
def new(self):
|
||||
return self.display_value(self._new)
|
||||
|
||||
@property
|
||||
def long(self):
|
||||
if isinstance(self.field, EmailField):
|
||||
return True
|
||||
return False
|
||||
|
||||
changes = []
|
||||
|
||||
for thisField in theFields:
|
||||
name = thisField.name
|
||||
|
||||
if name in excluded_keys:
|
||||
continue # if we're excluding this field, skip over it
|
||||
|
||||
oldValue = getattr(oldObj, name, None)
|
||||
newValue = getattr(newObj, name, None)
|
||||
|
||||
try:
|
||||
bothBlank = (not oldValue) and (not newValue)
|
||||
if oldValue != newValue and not bothBlank:
|
||||
compare = FieldCompare(thisField,oldValue,newValue)
|
||||
changes.append(compare)
|
||||
except TypeError: # logs issues with naive vs tz-aware datetimes
|
||||
logger.error('TypeError when comparing models')
|
||||
|
||||
return changes
|
||||
|
||||
def compare_event_items(old, new):
|
||||
# Recieves two event version objects and compares their items, returns an array of ItemCompare objects
|
||||
|
||||
item_type = ContentType.objects.get_for_model(models.EventItem)
|
||||
old_item_versions = old.revision.version_set.filter(content_type=item_type)
|
||||
new_item_versions = new.revision.version_set.filter(content_type=item_type)
|
||||
|
||||
class ItemCompare(object):
|
||||
def __init__(self, old=None, new=None, changes=None):
|
||||
self.old = old
|
||||
self.new = new
|
||||
self.changes = changes
|
||||
|
||||
# Build some dicts of what we have
|
||||
item_dict = {} # build a list of items, key is the item_pk
|
||||
for version in old_item_versions: # put all the old versions in a list
|
||||
compare = ItemCompare(old=version.object_version.object)
|
||||
item_dict[version.object_id] = compare
|
||||
|
||||
for version in new_item_versions: # go through the new versions
|
||||
try:
|
||||
compare = item_dict[version.object_id] # see if there's a matching old version
|
||||
compare.new = version.object_version.object # then add the new version to the dictionary
|
||||
except KeyError: # there's no matching old version, so add this item to the dictionary by itself
|
||||
compare = ItemCompare(new=version.object_version.object)
|
||||
|
||||
item_dict[version.object_id] = compare # update the dictionary with the changes
|
||||
|
||||
changes = []
|
||||
for (_, compare) in item_dict.items():
|
||||
compare.changes = model_compare(compare.old, compare.new, ['id','event','order']) # see what's changed
|
||||
if len(compare.changes) >= 1:
|
||||
changes.append(compare) # transfer into a sequential array to make it easier to deal with later
|
||||
|
||||
return changes
|
||||
|
||||
def get_versions_for_model(models):
|
||||
content_types = []
|
||||
for model in models:
|
||||
content_types.append(ContentType.objects.get_for_model(model))
|
||||
|
||||
versions = reversion.models.Version.objects.filter(
|
||||
content_type__in = content_types,
|
||||
).select_related("revision").order_by("-pk")
|
||||
|
||||
return versions
|
||||
|
||||
def get_previous_version(version):
|
||||
thisId = version.object_id
|
||||
thisVersionId = version.pk
|
||||
|
||||
versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId)
|
||||
|
||||
try:
|
||||
previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(field_name='revision__date_created')
|
||||
except ObjectDoesNotExist:
|
||||
return False
|
||||
|
||||
return previousVersions
|
||||
|
||||
def get_changes_for_version(newVersion, oldVersion=None):
|
||||
#Pass in a previous version if you already know it (for efficiancy)
|
||||
#if not provided then it will be looked up in the database
|
||||
|
||||
if oldVersion == None:
|
||||
oldVersion = get_previous_version(newVersion)
|
||||
|
||||
modelClass = newVersion.content_type.model_class()
|
||||
|
||||
compare = {
|
||||
'revision': newVersion.revision,
|
||||
'new': newVersion.object_version.object,
|
||||
'current': modelClass.objects.filter(pk=newVersion.pk).first(),
|
||||
'version': newVersion,
|
||||
|
||||
# Old things that may not be used
|
||||
'old': None,
|
||||
'field_changes': None,
|
||||
'item_changes': None,
|
||||
}
|
||||
|
||||
if oldVersion:
|
||||
compare['old'] = oldVersion.object_version.object
|
||||
compare['field_changes'] = model_compare(compare['old'], compare['new'])
|
||||
compare['item_changes'] = compare_event_items(oldVersion, newVersion)
|
||||
|
||||
return compare
|
||||
|
||||
class VersionHistory(generic.ListView):
|
||||
model = reversion.revisions.Version
|
||||
template_name = "RIGS/version_history.html"
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
thisModel = self.kwargs['model']
|
||||
|
||||
# thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk'])
|
||||
versions = reversion.get_for_object_reference(thisModel, self.kwargs['pk'])
|
||||
|
||||
return versions
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
thisModel = self.kwargs['model']
|
||||
|
||||
context = super(VersionHistory, self).get_context_data(**kwargs)
|
||||
|
||||
versions = context['object_list']
|
||||
thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk'])
|
||||
|
||||
items = []
|
||||
|
||||
for versionNo, thisVersion in enumerate(versions):
|
||||
if versionNo >= len(versions)-1:
|
||||
thisItem = get_changes_for_version(thisVersion, None)
|
||||
else:
|
||||
thisItem = get_changes_for_version(thisVersion, versions[versionNo+1])
|
||||
|
||||
items.append(thisItem)
|
||||
|
||||
context['object_list'] = items
|
||||
context['object'] = thisObject
|
||||
|
||||
return context
|
||||
|
||||
class ActivityTable(generic.ListView):
|
||||
model = reversion.revisions.Version
|
||||
template_name = "RIGS/activity_table.html"
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation])
|
||||
return versions
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
# Call the base implementation first to get a context
|
||||
context = super(ActivityTable, self).get_context_data(**kwargs)
|
||||
|
||||
items = []
|
||||
|
||||
for thisVersion in context['object_list']:
|
||||
thisItem = get_changes_for_version(thisVersion, None)
|
||||
items.append(thisItem)
|
||||
|
||||
context ['object_list'] = items
|
||||
|
||||
return context
|
||||
|
||||
class ActivityFeed(generic.ListView):
|
||||
model = reversion.revisions.Version
|
||||
template_name = "RIGS/activity_feed_data.html"
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation])
|
||||
return versions
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
maxTimeDelta = []
|
||||
|
||||
maxTimeDelta.append({ 'maxAge':datetime.timedelta(days=1), 'group':datetime.timedelta(hours=1)})
|
||||
maxTimeDelta.append({ 'maxAge':None, 'group':datetime.timedelta(days=1)})
|
||||
|
||||
# Call the base implementation first to get a context
|
||||
context = super(ActivityFeed, self).get_context_data(**kwargs)
|
||||
|
||||
items = []
|
||||
|
||||
for thisVersion in context['object_list']:
|
||||
thisItem = get_changes_for_version(thisVersion, None)
|
||||
if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None:
|
||||
thisItem['withPrevious'] = False
|
||||
if len(items)>=1:
|
||||
timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem['revision'].date_created
|
||||
timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created
|
||||
timeTogether = False
|
||||
for params in maxTimeDelta:
|
||||
if params['maxAge'] is None or timeAgo <= params['maxAge']:
|
||||
timeTogether = timeDiff < params['group']
|
||||
break
|
||||
|
||||
sameUser = thisItem['revision'].user == items[-1]['revision'].user
|
||||
thisItem['withPrevious'] = timeTogether & sameUser
|
||||
|
||||
items.append(thisItem)
|
||||
|
||||
context ['object_list'] = items
|
||||
|
||||
|
||||
return context
|
||||
@@ -10,6 +10,7 @@ import simplejson
|
||||
from django.contrib import messages
|
||||
import datetime
|
||||
import operator
|
||||
from registration.views import RegistrationView
|
||||
|
||||
from RIGS import models, forms
|
||||
|
||||
@@ -33,7 +34,6 @@ def login(request, **kwargs):
|
||||
|
||||
return login(request, authentication_form=forms.LoginForm)
|
||||
|
||||
|
||||
"""
|
||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
||||
May optionally also include some javascript in a success message to cause a load of
|
||||
@@ -68,6 +68,7 @@ class PersonDetail(generic.DetailView):
|
||||
|
||||
class PersonCreate(generic.CreateView):
|
||||
model = models.Person
|
||||
fields = ['name','phone','email','address','notes']
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
@@ -84,6 +85,7 @@ class PersonCreate(generic.CreateView):
|
||||
|
||||
class PersonUpdate(generic.UpdateView):
|
||||
model = models.Person
|
||||
fields = ['name','phone','email','address','notes']
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
@@ -120,6 +122,7 @@ class OrganisationDetail(generic.DetailView):
|
||||
|
||||
class OrganisationCreate(generic.CreateView):
|
||||
model = models.Organisation
|
||||
fields = ['name','phone','email','address','notes','union_account']
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
@@ -136,6 +139,7 @@ class OrganisationCreate(generic.CreateView):
|
||||
|
||||
class OrganisationUpdate(generic.UpdateView):
|
||||
model = models.Organisation
|
||||
fields = ['name','phone','email','address','notes','union_account']
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
@@ -172,6 +176,7 @@ class VenueDetail(generic.DetailView):
|
||||
|
||||
class VenueCreate(generic.CreateView):
|
||||
model = models.Venue
|
||||
fields = ['name','phone','email','address','notes']
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
@@ -188,6 +193,7 @@ class VenueCreate(generic.CreateView):
|
||||
|
||||
class VenueUpdate(generic.UpdateView):
|
||||
model = models.Venue
|
||||
fields = ['name','phone','email','address','notes']
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@@ -1,32 +1,31 @@
|
||||
Django==1.7.1
|
||||
Pillow==2.7.0
|
||||
PyMySQL==0.6.2
|
||||
PyPDF2==1.23
|
||||
Pygments==2.0.2
|
||||
dj-database-url==0.3.0
|
||||
dj-static==0.0.6
|
||||
django-debug-toolbar==1.2.2
|
||||
Django==1.8.2
|
||||
django-debug-toolbar==1.3.0
|
||||
django-ical==1.3
|
||||
django-recaptcha==1.0.4
|
||||
django-registration-redux==1.1
|
||||
django-reversion==1.8.5
|
||||
django-registration-redux==1.2
|
||||
django-reversion==1.8.7
|
||||
django-toolbelt==0.0.1
|
||||
django-widget-tweaks==1.3
|
||||
gunicorn==19.3.0
|
||||
icalendar==3.9.0
|
||||
lxml==3.4.1
|
||||
lxml==3.4.4
|
||||
Pillow==2.8.1
|
||||
psycopg2==2.6
|
||||
Pygments==2.0.2
|
||||
PyPDF2==1.24
|
||||
python-dateutil==2.4.2
|
||||
pytz==2015.2
|
||||
reportlab==2.7
|
||||
pytz==2015.4
|
||||
reportlab==3.1.44
|
||||
selenium==2.45.0
|
||||
simplejson==3.6.5
|
||||
simplejson==3.7.2
|
||||
six==1.9.0
|
||||
sqlparse==0.1.13
|
||||
static3==0.5.1
|
||||
sqlparse==0.1.15
|
||||
static3==0.6.1
|
||||
svg2rlg==0.3
|
||||
wsgiref==0.1.2
|
||||
yolk==0.4.3
|
||||
z3c.rml==2.7.2
|
||||
z3c.rml==2.8.1
|
||||
zope.event==4.0.3
|
||||
zope.interface==4.1.2
|
||||
zope.schema==4.4.2
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if not debug %}
|
||||
<script>
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
@@ -16,4 +17,5 @@
|
||||
{% endif %}
|
||||
ga('require', 'linkid', 'linkid.js');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load url from future %}
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
|
||||
<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="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}">
|
||||
<link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}">
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet' type='text/css'>
|
||||
|
||||
|
||||
@@ -42,25 +42,33 @@
|
||||
</div>
|
||||
<div class="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
{% if user.is_authenticated %}
|
||||
<li><a href="/">Home</a></li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Rigboard<b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'rigboard' %}">Rigboard</a></li>
|
||||
<li><a href="{% url 'event_archive' %}">Archive</a></li>
|
||||
<li><a href="{% url 'web_calendar' %}">Calendar</a></li>
|
||||
<li><a href="{% url 'event_create' %}">New</a></li>
|
||||
<li><a href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> Rigboard</a></li>
|
||||
<li><a href="{% url 'event_archive' %}"><span class="glyphicon glyphicon-book"></span> Archive</a></li>
|
||||
<li><a href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> Calendar</a></li>
|
||||
{% if perms.RIGS.view_event %}
|
||||
<li><a href="{% url 'activity_table' %}"><span class="glyphicon glyphicon-random"></span> Recent Changes</a></li>
|
||||
{% endif %}
|
||||
{% if perms.RIGS.add_event %}
|
||||
<li><a href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> New Event</a></li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.RIGS.view_invoice %}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'invoice_list' %}">Active</a></li>
|
||||
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Active</a></li>
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<li><a href="{% url 'invoice_waiting' %}">Waiting</a></li>
|
||||
<li><a href="{% url 'invoice_waiting' %}"><span class="glyphicon glyphicon-briefcase"></span> Waiting</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'invoice_archive' %}">Archive</a></li>
|
||||
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span> Archive</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -73,6 +81,7 @@
|
||||
{% if perms.RIGS.view_venue %}
|
||||
<li><a href="{% url 'venue_list' %}">Venues</a></li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
@@ -105,7 +114,7 @@
|
||||
{% else %}
|
||||
<a href="{% url "login" %}?next={{ request.path }}">
|
||||
<span class="icon-user"></span>
|
||||
Hi guest
|
||||
Login
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
@@ -159,16 +168,32 @@
|
||||
</script>
|
||||
<script src="{% static "js/dropdown.js" %}"></script>
|
||||
<script src="{% static "js/modal.js" %}"></script>
|
||||
<script src="{% static "js/konami.js" %}"></script>
|
||||
<script>
|
||||
jQuery(document).ready(function () {
|
||||
jQuery(document).on('click', '.modal-href', function (e) {
|
||||
e.preventDefault()
|
||||
modaltarget = jQuery(this).data('target')
|
||||
modalobject = "";
|
||||
jQuery('#modal').load(jQuery(this).attr('href'), function (e) {
|
||||
jQuery('#modal').modal();
|
||||
});
|
||||
$link = jQuery(this);
|
||||
// Anti modal inception
|
||||
if($link.parents('#modal').length == 0) {
|
||||
e.preventDefault();
|
||||
modaltarget = $link.data('target');
|
||||
modalobject = "";
|
||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
||||
jQuery('#modal').modal();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var easter_egg = new Konami();
|
||||
easter_egg.code = function() {
|
||||
var s = document.createElement('script');
|
||||
s.type='text/javascript';
|
||||
document.body.appendChild(s);
|
||||
s.src='{% static "js/asteroids.min.js"%}';
|
||||
ga('send', 'event', 'easter_egg', 'activated');
|
||||
}
|
||||
easter_egg.load();
|
||||
});
|
||||
</script>
|
||||
{% block js %}
|
||||
|
||||
2
templates/base_ajax_nomodal.html
Normal file
2
templates/base_ajax_nomodal.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% block js %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load url from future %}
|
||||
|
||||
Welcome {{ user }},
|
||||
|
||||
Thank you for registering on {{ site }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load url from future %}
|
||||
|
||||
{% load widget_tweaks %}
|
||||
{% include 'form_errors.html' %}
|
||||
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load url from future %}
|
||||
|
||||
|
||||
{% block title %}Change Password{% endblock %}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load widget_tweaks %}
|
||||
{% load url from future %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load i18n %}{% load url from future %}{% autoescape off %}
|
||||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
|
||||
|
||||
{% trans "Please go to the following page and choose a new password:" %}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import re
|
||||
from django.template import Library, Node, Variable, TemplateSyntaxError
|
||||
register = Library()
|
||||
|
||||
|
||||
def silence_without_field(fn):
|
||||
def wrapped(field, attr):
|
||||
if not field:
|
||||
return ""
|
||||
return fn(field, attr)
|
||||
return wrapped
|
||||
|
||||
|
||||
def _process_field_attributes(field, attr, process):
|
||||
|
||||
# split attribute name and value from 'attr:value' string
|
||||
params = attr.split(':', 1)
|
||||
attribute = params[0]
|
||||
value = params[1] if len(params) == 2 else ''
|
||||
|
||||
# decorate field.as_widget method with updated attributes
|
||||
old_as_widget = field.as_widget
|
||||
|
||||
def as_widget(self, widget=None, attrs=None, only_initial=False):
|
||||
attrs = attrs or {}
|
||||
process(widget or self.field.widget, attrs, attribute, value)
|
||||
return old_as_widget(widget, attrs, only_initial)
|
||||
|
||||
bound_method = type(old_as_widget)
|
||||
try:
|
||||
field.as_widget = bound_method(as_widget, field, field.__class__)
|
||||
except TypeError: # python 3
|
||||
field.as_widget = bound_method(as_widget, field)
|
||||
return field
|
||||
|
||||
|
||||
@register.filter("attr")
|
||||
@silence_without_field
|
||||
def set_attr(field, attr):
|
||||
|
||||
def process(widget, attrs, attribute, value):
|
||||
attrs[attribute] = value
|
||||
|
||||
return _process_field_attributes(field, attr, process)
|
||||
|
||||
|
||||
@register.filter("add_error_attr")
|
||||
@silence_without_field
|
||||
def add_error_attr(field, attr):
|
||||
if hasattr(field, 'errors') and field.errors:
|
||||
return set_attr(field, attr)
|
||||
return field
|
||||
|
||||
|
||||
@register.filter("append_attr")
|
||||
@silence_without_field
|
||||
def append_attr(field, attr):
|
||||
def process(widget, attrs, attribute, value):
|
||||
if attrs.get(attribute):
|
||||
attrs[attribute] += ' ' + value
|
||||
elif widget.attrs.get(attribute):
|
||||
attrs[attribute] = widget.attrs[attribute] + ' ' + value
|
||||
else:
|
||||
attrs[attribute] = value
|
||||
return _process_field_attributes(field, attr, process)
|
||||
|
||||
|
||||
@register.filter("add_class")
|
||||
@silence_without_field
|
||||
def add_class(field, css_class):
|
||||
return append_attr(field, 'class:' + css_class)
|
||||
|
||||
|
||||
@register.filter("add_error_class")
|
||||
@silence_without_field
|
||||
def add_error_class(field, css_class):
|
||||
if hasattr(field, 'errors') and field.errors:
|
||||
return add_class(field, css_class)
|
||||
return field
|
||||
|
||||
|
||||
@register.filter("set_data")
|
||||
@silence_without_field
|
||||
def set_data(field, data):
|
||||
return set_attr(field, 'data-' + data)
|
||||
|
||||
|
||||
@register.filter(name='field_type')
|
||||
def field_type(field):
|
||||
"""
|
||||
Template filter that returns field class name (in lower case).
|
||||
E.g. if field is CharField then {{ field|field_type }} will
|
||||
return 'charfield'.
|
||||
"""
|
||||
if hasattr(field, 'field') and field.field:
|
||||
return field.field.__class__.__name__.lower()
|
||||
return ''
|
||||
|
||||
|
||||
@register.filter(name='widget_type')
|
||||
def widget_type(field):
|
||||
"""
|
||||
Template filter that returns field widget class name (in lower case).
|
||||
E.g. if field's widget is TextInput then {{ field|widget_type }} will
|
||||
return 'textinput'.
|
||||
"""
|
||||
if hasattr(field, 'field') and hasattr(field.field, 'widget') and field.field.widget:
|
||||
return field.field.widget.__class__.__name__.lower()
|
||||
return ''
|
||||
|
||||
|
||||
# ======================== render_field tag ==============================
|
||||
|
||||
ATTRIBUTE_RE = re.compile(r"""
|
||||
(?P<attr>
|
||||
[\w_-]+
|
||||
)
|
||||
(?P<sign>
|
||||
\+?=
|
||||
)
|
||||
(?P<value>
|
||||
['"]? # start quote
|
||||
[^"']*
|
||||
['"]? # end quote
|
||||
)
|
||||
""", re.VERBOSE | re.UNICODE)
|
||||
|
||||
@register.tag
|
||||
def render_field(parser, token):
|
||||
"""
|
||||
Render a form field using given attribute-value pairs
|
||||
|
||||
Takes form field as first argument and list of attribute-value pairs for
|
||||
all other arguments. Attribute-value pairs should be in the form of
|
||||
attribute=value or attribute="a value" for assignment and attribute+=value
|
||||
or attribute+="value" for appending.
|
||||
"""
|
||||
error_msg = '%r tag requires a form field followed by a list of attributes and values in the form attr="value"' % token.split_contents()[0]
|
||||
try:
|
||||
bits = token.split_contents()
|
||||
tag_name = bits[0]
|
||||
form_field = bits[1]
|
||||
attr_list = bits[2:]
|
||||
except ValueError:
|
||||
raise TemplateSyntaxError(error_msg)
|
||||
|
||||
form_field = parser.compile_filter(form_field)
|
||||
|
||||
set_attrs = []
|
||||
append_attrs = []
|
||||
for pair in attr_list:
|
||||
match = ATTRIBUTE_RE.match(pair)
|
||||
if not match:
|
||||
raise TemplateSyntaxError(error_msg + ": %s" % pair)
|
||||
dct = match.groupdict()
|
||||
attr, sign, value = dct['attr'], dct['sign'], parser.compile_filter(dct['value'])
|
||||
if sign == "=":
|
||||
set_attrs.append((attr, value))
|
||||
else:
|
||||
append_attrs.append((attr, value))
|
||||
|
||||
return FieldAttributeNode(form_field, set_attrs, append_attrs)
|
||||
|
||||
|
||||
class FieldAttributeNode(Node):
|
||||
def __init__(self, field, set_attrs, append_attrs):
|
||||
self.field = field
|
||||
self.set_attrs = set_attrs
|
||||
self.append_attrs = append_attrs
|
||||
|
||||
def render(self, context):
|
||||
bounded_field = self.field.resolve(context)
|
||||
field = getattr(bounded_field, 'field', None)
|
||||
if (getattr(bounded_field, 'errors', None) and
|
||||
'WIDGET_ERROR_CLASS' in context):
|
||||
bounded_field = append_attr(bounded_field, 'class:%s' %
|
||||
context['WIDGET_ERROR_CLASS'])
|
||||
if field and field.required and 'WIDGET_REQUIRED_CLASS' in context:
|
||||
bounded_field = append_attr(bounded_field, 'class:%s' %
|
||||
context['WIDGET_REQUIRED_CLASS'])
|
||||
for k, v in self.set_attrs:
|
||||
bounded_field = set_attr(bounded_field, '%s:%s' % (k,v.resolve(context)))
|
||||
for k, v in self.append_attrs:
|
||||
bounded_field = append_attr(bounded_field, '%s:%s' % (k,v.resolve(context)))
|
||||
return bounded_field
|
||||
@@ -1,333 +0,0 @@
|
||||
import string
|
||||
try:
|
||||
from django.utils.unittest import expectedFailure
|
||||
except ImportError:
|
||||
def expectedFailure(func):
|
||||
return lambda *args, **kwargs: None
|
||||
|
||||
from django.test import TestCase
|
||||
from django.forms import Form, CharField, TextInput
|
||||
from django import forms
|
||||
from django.template import Template, Context
|
||||
from django.forms.extras.widgets import SelectDateWidget
|
||||
|
||||
# ==============================
|
||||
# Testing helpers
|
||||
# ==============================
|
||||
|
||||
class MyForm(Form):
|
||||
"""
|
||||
Test form. If you want to test rendering of a field,
|
||||
add it to this form and use one of 'render_...' functions
|
||||
from this module.
|
||||
"""
|
||||
simple = CharField()
|
||||
with_attrs = CharField(widget=TextInput(attrs={
|
||||
'foo': 'baz',
|
||||
'egg': 'spam'
|
||||
}))
|
||||
with_cls = CharField(widget=TextInput(attrs={'class':'class0'}))
|
||||
date = forms.DateField(widget=SelectDateWidget(attrs={'egg': 'spam'}))
|
||||
|
||||
|
||||
def render_form(text, form=None, **context_args):
|
||||
"""
|
||||
Renders template ``text`` with widget_tweaks library loaded
|
||||
and MyForm instance available in context as ``form``.
|
||||
"""
|
||||
tpl = Template("{% load widget_tweaks %}" + text)
|
||||
context_args.update({'form': MyForm() if form is None else form})
|
||||
context = Context(context_args)
|
||||
return tpl.render(context)
|
||||
|
||||
|
||||
def render_field(field, template_filter, params, *args, **kwargs):
|
||||
"""
|
||||
Renders ``field`` of MyForm with filter ``template_filter`` applied.
|
||||
``params`` are filter arguments.
|
||||
|
||||
If you want to apply several filters (in a chain),
|
||||
pass extra ``template_filter`` and ``params`` as positional arguments.
|
||||
|
||||
In order to use custom form, pass form instance as ``form``
|
||||
keyword argument.
|
||||
"""
|
||||
filters = [(template_filter, params)]
|
||||
filters.extend(zip(args[::2], args[1::2]))
|
||||
filter_strings = ['|%s:"%s"' % (f[0], f[1],) for f in filters]
|
||||
render_field_str = '{{ form.%s%s }}' % (field, ''.join(filter_strings))
|
||||
return render_form(render_field_str, **kwargs)
|
||||
|
||||
|
||||
def render_field_from_tag(field, *attributes):
|
||||
"""
|
||||
Renders MyForm's field ``field`` with attributes passed
|
||||
as positional arguments.
|
||||
"""
|
||||
attr_strings = [' %s' % f for f in attributes]
|
||||
tpl = string.Template('{% render_field form.$field$attrs %}')
|
||||
render_field_str = tpl.substitute(field=field, attrs=''.join(attr_strings))
|
||||
return render_form(render_field_str)
|
||||
|
||||
|
||||
def assertIn(value, obj):
|
||||
assert value in obj, "%s not in %s" % (value, obj,)
|
||||
|
||||
|
||||
def assertNotIn(value, obj):
|
||||
assert value not in obj, "%s in %s" % (value, obj,)
|
||||
|
||||
|
||||
# ===============================
|
||||
# Test cases
|
||||
# ===============================
|
||||
|
||||
class SimpleAttrTest(TestCase):
|
||||
def test_attr(self):
|
||||
res = render_field('simple', 'attr', 'foo:bar')
|
||||
assertIn('type="text"', res)
|
||||
assertIn('name="simple"', res)
|
||||
assertIn('id="id_simple"', res)
|
||||
assertIn('foo="bar"', res)
|
||||
|
||||
def test_attr_chaining(self):
|
||||
res = render_field('simple', 'attr', 'foo:bar', 'attr', 'bar:baz')
|
||||
assertIn('type="text"', res)
|
||||
assertIn('name="simple"', res)
|
||||
assertIn('id="id_simple"', res)
|
||||
assertIn('foo="bar"', res)
|
||||
assertIn('bar="baz"', res)
|
||||
|
||||
def test_add_class(self):
|
||||
res = render_field('simple', 'add_class', 'foo')
|
||||
assertIn('class="foo"', res)
|
||||
|
||||
def test_add_multiple_classes(self):
|
||||
res = render_field('simple', 'add_class', 'foo bar')
|
||||
assertIn('class="foo bar"', res)
|
||||
|
||||
def test_add_class_chaining(self):
|
||||
res = render_field('simple', 'add_class', 'foo', 'add_class', 'bar')
|
||||
assertIn('class="bar foo"', res)
|
||||
|
||||
def test_set_data(self):
|
||||
res = render_field('simple', 'set_data', 'key:value')
|
||||
assertIn('data-key="value"', res)
|
||||
|
||||
|
||||
class ErrorsTest(TestCase):
|
||||
|
||||
def _err_form(self):
|
||||
form = MyForm({'foo': 'bar'}) # some random data
|
||||
form.is_valid() # trigger form validation
|
||||
return form
|
||||
|
||||
def test_error_class_no_error(self):
|
||||
res = render_field('simple', 'add_error_class', 'err')
|
||||
assertNotIn('class="err"', res)
|
||||
|
||||
def test_error_class_error(self):
|
||||
form = self._err_form()
|
||||
res = render_field('simple', 'add_error_class', 'err', form=form)
|
||||
assertIn('class="err"', res)
|
||||
|
||||
def test_error_attr_no_error(self):
|
||||
res = render_field('simple', 'add_error_attr', 'aria-invalid:true')
|
||||
assertNotIn('aria-invalid="true"', res)
|
||||
|
||||
def test_error_attr_error(self):
|
||||
form = self._err_form()
|
||||
res = render_field('simple', 'add_error_attr', 'aria-invalid:true', form=form)
|
||||
assertIn('aria-invalid="true"', res)
|
||||
|
||||
|
||||
class SilenceTest(TestCase):
|
||||
def test_silence_without_field(self):
|
||||
res = render_field("nothing", 'attr', 'foo:bar')
|
||||
self.assertEqual(res, "")
|
||||
res = render_field("nothing", 'add_class', 'some')
|
||||
self.assertEqual(res, "")
|
||||
|
||||
|
||||
class CustomizedWidgetTest(TestCase):
|
||||
def test_attr(self):
|
||||
res = render_field('with_attrs', 'attr', 'foo:bar')
|
||||
assertIn('foo="bar"', res)
|
||||
assertNotIn('foo="baz"', res)
|
||||
assertIn('egg="spam"', res)
|
||||
|
||||
# see https://code.djangoproject.com/ticket/16754
|
||||
@expectedFailure
|
||||
def test_selectdatewidget(self):
|
||||
res = render_field('date', 'attr', 'foo:bar')
|
||||
assertIn('egg="spam"', res)
|
||||
assertIn('foo="bar"', res)
|
||||
|
||||
def test_attr_chaining(self):
|
||||
res = render_field('with_attrs', 'attr', 'foo:bar', 'attr', 'bar:baz')
|
||||
assertIn('foo="bar"', res)
|
||||
assertNotIn('foo="baz"', res)
|
||||
assertIn('egg="spam"', res)
|
||||
assertIn('bar="baz"', res)
|
||||
|
||||
def test_attr_class(self):
|
||||
res = render_field('with_cls', 'attr', 'foo:bar')
|
||||
assertIn('foo="bar"', res)
|
||||
assertIn('class="class0"', res)
|
||||
|
||||
def test_default_attr(self):
|
||||
res = render_field('with_cls', 'attr', 'type:search')
|
||||
assertIn('class="class0"', res)
|
||||
assertIn('type="search"', res)
|
||||
|
||||
def test_add_class(self):
|
||||
res = render_field('with_cls', 'add_class', 'class1')
|
||||
assertIn('class0', res)
|
||||
assertIn('class1', res)
|
||||
|
||||
def test_add_class_chaining(self):
|
||||
res = render_field('with_cls', 'add_class', 'class1', 'add_class', 'class2')
|
||||
assertIn('class0', res)
|
||||
assertIn('class1', res)
|
||||
assertIn('class2', res)
|
||||
|
||||
|
||||
class FieldReuseTest(TestCase):
|
||||
|
||||
def test_field_double_rendering_simple(self):
|
||||
res = render_form('{{ form.simple }}{{ form.simple|attr:"foo:bar" }}{{ form.simple }}')
|
||||
self.assertEqual(res.count("bar"), 1)
|
||||
|
||||
def test_field_double_rendering_simple_css(self):
|
||||
res = render_form('{{ form.simple }}{{ form.simple|add_class:"bar" }}{{ form.simple|add_class:"baz" }}')
|
||||
self.assertEqual(res.count("baz"), 1)
|
||||
self.assertEqual(res.count("bar"), 1)
|
||||
|
||||
def test_field_double_rendering_attrs(self):
|
||||
res = render_form('{{ form.with_cls }}{{ form.with_cls|add_class:"bar" }}{{ form.with_cls }}')
|
||||
self.assertEqual(res.count("class0"), 3)
|
||||
self.assertEqual(res.count("bar"), 1)
|
||||
|
||||
|
||||
class SimpleRenderFieldTagTest(TestCase):
|
||||
def test_attr(self):
|
||||
res = render_field_from_tag('simple', 'foo="bar"')
|
||||
assertIn('type="text"', res)
|
||||
assertIn('name="simple"', res)
|
||||
assertIn('id="id_simple"', res)
|
||||
assertIn('foo="bar"', res)
|
||||
|
||||
def test_multiple_attrs(self):
|
||||
res = render_field_from_tag('simple', 'foo="bar"', 'bar="baz"')
|
||||
assertIn('type="text"', res)
|
||||
assertIn('name="simple"', res)
|
||||
assertIn('id="id_simple"', res)
|
||||
assertIn('foo="bar"', res)
|
||||
assertIn('bar="baz"', res)
|
||||
|
||||
|
||||
class RenderFieldTagSilenceTest(TestCase):
|
||||
def test_silence_without_field(self):
|
||||
res = render_field_from_tag("nothing", 'foo="bar"')
|
||||
self.assertEqual(res, "")
|
||||
res = render_field_from_tag("nothing", 'class+="some"')
|
||||
self.assertEqual(res, "")
|
||||
|
||||
|
||||
class RenderFieldTagCustomizedWidgetTest(TestCase):
|
||||
def test_attr(self):
|
||||
res = render_field_from_tag('with_attrs', 'foo="bar"')
|
||||
assertIn('foo="bar"', res)
|
||||
assertNotIn('foo="baz"', res)
|
||||
assertIn('egg="spam"', res)
|
||||
|
||||
# see https://code.djangoproject.com/ticket/16754
|
||||
@expectedFailure
|
||||
def test_selectdatewidget(self):
|
||||
res = render_field_from_tag('date', 'foo="bar"')
|
||||
assertIn('egg="spam"', res)
|
||||
assertIn('foo="bar"', res)
|
||||
|
||||
def test_multiple_attrs(self):
|
||||
res = render_field_from_tag('with_attrs', 'foo="bar"', 'bar="baz"')
|
||||
assertIn('foo="bar"', res)
|
||||
assertNotIn('foo="baz"', res)
|
||||
assertIn('egg="spam"', res)
|
||||
assertIn('bar="baz"', res)
|
||||
|
||||
def test_attr_class(self):
|
||||
res = render_field_from_tag('with_cls', 'foo="bar"')
|
||||
assertIn('foo="bar"', res)
|
||||
assertIn('class="class0"', res)
|
||||
|
||||
def test_default_attr(self):
|
||||
res = render_field_from_tag('with_cls', 'type="search"')
|
||||
assertIn('class="class0"', res)
|
||||
assertIn('type="search"', res)
|
||||
|
||||
def test_append_attr(self):
|
||||
res = render_field_from_tag('with_cls', 'class+="class1"')
|
||||
assertIn('class0', res)
|
||||
assertIn('class1', res)
|
||||
|
||||
def test_duplicate_append_attr(self):
|
||||
res = render_field_from_tag('with_cls', 'class+="class1"', 'class+="class2"')
|
||||
assertIn('class0', res)
|
||||
assertIn('class1', res)
|
||||
assertIn('class2', res)
|
||||
|
||||
def test_hyphenated_attributes(self):
|
||||
res = render_field_from_tag('with_cls', 'data-foo="bar"')
|
||||
assertIn('data-foo="bar"', res)
|
||||
assertIn('class="class0"', res)
|
||||
|
||||
|
||||
class RenderFieldWidgetClassesTest(TestCase):
|
||||
def test_use_widget_required_class(self):
|
||||
res = render_form('{% render_field form.simple %}',
|
||||
WIDGET_REQUIRED_CLASS='required_class')
|
||||
self.assertIn('class="required_class"', res)
|
||||
|
||||
def test_use_widget_error_class(self):
|
||||
res = render_form('{% render_field form.simple %}', form=MyForm({}),
|
||||
WIDGET_ERROR_CLASS='error_class')
|
||||
self.assertIn('class="error_class"', res)
|
||||
|
||||
def test_use_widget_error_class_with_other_classes(self):
|
||||
res = render_form('{% render_field form.simple class="blue" %}',
|
||||
form=MyForm({}), WIDGET_ERROR_CLASS='error_class')
|
||||
self.assertIn('class="blue error_class"', res)
|
||||
|
||||
def test_use_widget_required_class_with_other_classes(self):
|
||||
res = render_form('{% render_field form.simple class="blue" %}',
|
||||
form=MyForm({}), WIDGET_REQUIRED_CLASS='required_class')
|
||||
self.assertIn('class="blue required_class"', res)
|
||||
|
||||
|
||||
class RenderFieldTagFieldReuseTest(TestCase):
|
||||
def test_field_double_rendering_simple(self):
|
||||
res = render_form('{{ form.simple }}{% render_field form.simple foo="bar" %}{% render_field form.simple %}')
|
||||
self.assertEqual(res.count("bar"), 1)
|
||||
|
||||
def test_field_double_rendering_simple_css(self):
|
||||
res = render_form('{% render_field form.simple %}{% render_field form.simple class+="bar" %}{% render_field form.simple class+="baz" %}')
|
||||
self.assertEqual(res.count("baz"), 1)
|
||||
self.assertEqual(res.count("bar"), 1)
|
||||
|
||||
def test_field_double_rendering_attrs(self):
|
||||
res = render_form('{% render_field form.with_cls %}{% render_field form.with_cls class+="bar" %}{% render_field form.with_cls %}')
|
||||
self.assertEqual(res.count("class0"), 3)
|
||||
self.assertEqual(res.count("bar"), 1)
|
||||
|
||||
|
||||
class RenderFieldTagUseTemplateVariableTest(TestCase):
|
||||
def test_use_template_variable_in_parametrs(self):
|
||||
res = render_form('{% render_field form.with_attrs egg+="pahaz" placeholder=form.with_attrs.label %}')
|
||||
assertIn('egg="spam pahaz"', res)
|
||||
assertIn('placeholder="With attrs"', res)
|
||||
|
||||
|
||||
class RenderFieldFilter_field_type_widget_type_Test(TestCase):
|
||||
def test_field_type_widget_type_rendering_simple(self):
|
||||
res = render_form('<div class="{{ form.simple|field_type }} {{ form.simple|widget_type }} {{ form.simple.html_name }}">{{ form.simple }}</div>')
|
||||
assertIn('class="charfield textinput simple"', res)
|
||||
Reference in New Issue
Block a user