Merge branch 'master' into paperwork

This commit is contained in:
David Taylor
2015-06-29 12:56:25 +01:00
64 changed files with 2145 additions and 786 deletions

View File

View File

View 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')

View File

@@ -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 = (

View File

@@ -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')),

View File

@@ -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.

View File

@@ -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()

View File

@@ -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.

View File

@@ -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

View 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),
),
]

View File

@@ -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
View 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)

View File

@@ -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

View File

@@ -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
View 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
View 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;
};

View 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);

View File

@@ -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;

View File

@@ -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);
};
}
}

View 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>

View 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 %}

View 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 %}

View File

@@ -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>&nbsp;</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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>£&nbsp;<span class="cost"></span></td>

View 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>

View File

@@ -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">

View File

@@ -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">

View 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>

View File

@@ -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="row">
<div class="col-md-10 col-md-offset-1">
<div class="col-sm-12">
<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>
<div class="col-sm-6">
<h3>{{object.name}}</h3>
</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>
</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>Last Name</dt>
<dd>{{object.last_name}}</dd>
<dt>Email</dt>
<dd>{{object.email}}</dd>
<dt>Email</dt>
<dd>{{object.email}}</dd>
<dt>Last Login</dt>
<dd>{{object.last_login|date:"d/m/Y H:i"}}</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>Date Joined</dt>
<dd>{{object.date_joined|date:"d/m/Y H:i"}}</dd>
<dt>Initials</dt>
<dd>{{object.initials}}</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 %}
<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>
<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>
<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>
<h4>Personal iCal Details</h4>
<div class="col-sm-3">
<div class="center-block">
<img src="{{object.profile_picture}}" class="img-responsive img-rounded" />
</div>
<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 class="row">
<div class="col-sm-12">
<h4>Events</h4>
{% with object.latest_events as events %}
{% include 'RIGS/event_table.html' %}
{% endwith %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -9,10 +9,13 @@
<div class="col-sm-10">
<h3>Rigboard</h3>
</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">

View File

@@ -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">

View 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 %}

View 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 %}

View File

@@ -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
View 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
View 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'))

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -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
View 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

View File

@@ -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():

Binary file not shown.

View File

@@ -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

View File

@@ -1,3 +1,4 @@
{% if not debug %}
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
@@ -17,3 +18,4 @@
ga('require', 'linkid', 'linkid.js');
ga('send', 'pageview');
</script>
{% endif %}

View File

@@ -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 %}

View File

@@ -0,0 +1,2 @@
{% block js %}{% endblock %}
{% block content %}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% load url from future %}
Welcome {{ user }},
Thank you for registering on {{ site }}

View File

@@ -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">

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% load url from future %}
{% block breadcrumbs %}
<div class="breadcrumbs">

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load widget_tweaks %}
{% load url from future %}
{% block title %}Change Password{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% load url from future %}
{% block breadcrumbs %}
<div class="breadcrumbs">

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% load url from future %}
{% block breadcrumbs %}
<div class="breadcrumbs">

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% load url from future %}
{% block breadcrumbs %}
<div class="breadcrumbs">

View File

@@ -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:" %}

View File

@@ -1 +0,0 @@

View File

@@ -1 +0,0 @@

View File

@@ -1 +0,0 @@

View File

@@ -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

View File

@@ -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)