Merge branch master into assets-testing

# Conflicts:
#	RIGS/test_functional.py
This commit is contained in:
2020-02-07 22:18:21 +00:00
12 changed files with 292 additions and 276 deletions

View File

@@ -1,20 +1,13 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
[![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS) [![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Dependency Status](https://gemnasium.com/badges/github.com/nottinghamtec/PyRIGS.svg)](https://gemnasium.com/github.com/nottinghamtec/PyRIGS)
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
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. 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.
At this stage the project is very early on, and the main focus has been on getting a working system that can be tested and put into use ASAP due to the imminent failure of the existing system. Because of this, the documentation is still quite weak, but this should be fixed as time goes on.
This document is intended to get you up and running, but if don't care about what I have to say, just clone the sodding repository and have a poke around with what's in it, but for GODS SAKE DO NOT PUSH WITHOUT TESTING.
### What is this repository for? ### ### What is this repository for? ###
For the rapid development of the application for medium term deployment, the main branch is being used. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
Once the application is deployed in a production environment, other branches should be used to properly stage edits and pushes of new features. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework. Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
@@ -26,7 +19,7 @@ For the more experienced developer/somebody who doesn't want a full IDE and want
Please contact TJP for details on how to acquire these. Please contact TJP for details on how to acquire these.
### Python Environment ### ### Python Environment ###
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Mainly the C implementation of Python 2 (CPython 2) has been used (specifically the Python 2.7 standard). Most of the application has been written with Python 3 in mind however, and should run without issue. Some level of testing on Python 3 has been done, but there is no guarantee it will work (for more information on this please see [[Python Version]] on the wiki) Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Orginally written with the C implementation of Python 2 (CPython 2, specifically the Python 2.7 standard), the application now runs in Python 3.
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment. Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
@@ -115,5 +108,4 @@ python manage.py test RIGS.test_models.EventTestCase.test_current_events
``` ```
### Committing, pushing and testing ### [![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com)
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.

View File

@@ -22,7 +22,7 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
class Meta: class Meta:
model = models.Profile model = models.Profile
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone') fields = ('username', 'email', 'first_name', 'last_name', 'initials')
def clean_initials(self): def clean_initials(self):
""" """
@@ -129,6 +129,11 @@ class EventForm(forms.ModelForm):
return item return item
def clean(self):
if self.cleaned_data.get("is_rig") and not (self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
raise forms.ValidationError('You haven\'t provided any client contact details. Please add a person or organisation.', code='contact')
return super(EventForm, self).clean()
def save(self, commit=True): def save(self, commit=True):
m = super(EventForm, self).save(commit=False) m = super(EventForm, self).save(commit=False)

View File

@@ -8,7 +8,7 @@ def user_created(sender, user, request, **kwargs):
user.first_name = form.data['first_name'] user.first_name = form.data['first_name']
user.last_name = form.data['last_name'] user.last_name = form.data['last_name']
user.initials = form.data['initials'] user.initials = form.data['initials']
user.phone = form.data['phone'] # user.phone = form.data['phone']
user.save() user.save()

View File

@@ -140,14 +140,17 @@ class EventUpdate(generic.UpdateView):
if value is not None and value != '': if value is not None and value != '':
context[field] = model.objects.get(pk=value) context[field] = model.objects.get(pk=value)
return context
def render_to_response(self, context, **response_kwargs):
if not hasattr(context, 'duplicate'):
# If this event has already been emailed to a client, show a warning # If this event has already been emailed to a client, show a warning
if self.object.auth_request_at is not None: if self.object.auth_request_at is not None:
messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.') messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
if hasattr(self.object, 'authorised'): if hasattr(self.object, 'authorised'):
messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.') messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
return context
def get_success_url(self): def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -65,6 +65,15 @@ textarea {
overflow: hidden; overflow: hidden;
} }
.dont-break-out {
overflow-wrap: break-word;
word-wrap: break-word;
-webkit-hyphens: auto;
-ms-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
.modal-dialog { .modal-dialog {
z-index: inherit; // bug fix introduced in 52682ce z-index: inherit; // bug fix introduced in 52682ce
} }

View File

@@ -122,7 +122,7 @@
<dd>&nbsp;</dd> <dd>&nbsp;</dd>
<dt>Event Description</dt> <dt>Event Description</dt>
<dd>{{ event.description|linebreaksbr }}</dd> <dd class="dont-break-out">{{ event.description|linebreaksbr }}</dd>
<dd>&nbsp;</dd> <dd>&nbsp;</dd>
@@ -158,7 +158,15 @@
</div> </div>
{% if event.is_rig and event.internal %} {% if event.is_rig and event.internal %}
<div class="col-sm-12"> <div class="col-sm-12">
<div class="panel panel-default"> <div class="panel panel-default
{% if object.authorised %}
panel-success
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
panel-warning
{% elif event.auth_request_to %}
panel-info
{% endif %}
">
<div class="panel-heading">Client Authorisation</div> <div class="panel-heading">Client Authorisation</div>
<div class="panel-body"> <div class="panel-body">
<dl class="dl-horizontal col-sm-6"> <dl class="dl-horizontal col-sm-6">
@@ -188,7 +196,7 @@
</dd> </dd>
<dt>Authorised at</dt> <dt>Authorised at</dt>
<dd>{{ object.authorisation.last_edited_at }}</dd> <dd>{{ object.authorisation.last_edited_at|date:"D d M Y H:i" }}</dd>
<dt>Authorised amount</dt> <dt>Authorised amount</dt>
<dd> <dd>
@@ -216,7 +224,7 @@
<div class="panel-body"> <div class="panel-body">
<div class="well well-sm"> <div class="well well-sm">
<h4>Notes</h4> <h4>Notes</h4>
{{ event.notes|linebreaksbr }} <div class="dont-break-out">{{ event.notes|linebreaksbr }}</div>
</div> </div>
{% include 'RIGS/item_table.html' %} {% include 'RIGS/item_table.html' %}
</div> </div>

View File

@@ -14,7 +14,7 @@
{% for change in itemChange.field_changes %} {% for change in itemChange.field_changes %}
<li class="list-group-item"> <li class="list-group-item">
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4> <h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
{% include "RIGS/version_changes_change.html" %} <div class="dont-break-out">{% include "RIGS/version_changes_change.html" %}</div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -2,11 +2,11 @@
{% if change.linebreaks and change.new and change.old %} {% if change.linebreaks and change.new and change.old %}
{% for diff in change.diff %} {% for diff in change.diff %}
{% if diff.type == "insert" %} {% if diff.type == "insert" %}
<ins>{{ diff.text|linebreaksbr }}</ins> <ins class="dont-break-out">{{ diff.text|linebreaksbr }}</ins>
{% elif diff.type == "delete" %} {% elif diff.type == "delete" %}
<del>{{diff.text|linebreaksbr}}</del> <del class="dont-break-out">{{diff.text|linebreaksbr}}</del>
{% else %} {% else %}
<span>{{diff.text|linebreaksbr}}</span> <span class="dont-break-out">{{diff.text|linebreaksbr}}</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -1,17 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import re import re
import pytz
from datetime import date, time, datetime, timedelta from datetime import date, time, datetime, timedelta
import pytz
from django.core import mail from django.conf import settings
from django.core import mail, signing
from django.db import transaction from django.db import transaction
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.test import LiveServerTestCase, TestCase from django.test import LiveServerTestCase, TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from reversion import revisions as reversion
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
@@ -63,8 +66,9 @@ class UserRegistrationTest(LiveServerTestCase):
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name') self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
initials = self.browser.find_element_by_id('id_initials') initials = self.browser.find_element_by_id('id_initials')
self.assertEqual(initials.get_attribute('placeholder'), 'Initials') self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
phone = self.browser.find_element_by_id('id_phone') # No longer required for new users
self.assertEqual(phone.get_attribute('placeholder'), 'Phone') # phone = self.browser.find_element_by_id('id_phone')
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
# Fill the form out incorrectly # Fill the form out incorrectly
username.send_keys('TestUsername') username.send_keys('TestUsername')
@@ -75,7 +79,7 @@ class UserRegistrationTest(LiveServerTestCase):
first_name.send_keys('John') first_name.send_keys('John')
last_name.send_keys('Smith') last_name.send_keys('Smith')
initials.send_keys('JS') initials.send_keys('JS')
phone.send_keys('0123456789') # phone.send_keys('0123456789')
self.browser.execute_script( self.browser.execute_script(
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()") "return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
@@ -153,7 +157,7 @@ class UserRegistrationTest(LiveServerTestCase):
self.assertEqual(profileObject.first_name, 'John') self.assertEqual(profileObject.first_name, 'John')
self.assertEqual(profileObject.last_name, 'Smith') self.assertEqual(profileObject.last_name, 'Smith')
self.assertEqual(profileObject.initials, 'JS') self.assertEqual(profileObject.initials, 'JS')
self.assertEqual(profileObject.phone, '0123456789') # self.assertEqual(profileObject.phone, '0123456789')
self.assertEqual(profileObject.email, 'test@example.com') self.assertEqual(profileObject.email, 'test@example.com')
# All is well # All is well
@@ -208,7 +212,6 @@ class EventTest(LiveServerTestCase):
self.browser.get(self.live_server_url + '/rigboard/') self.browser.get(self.live_server_url + '/rigboard/')
def testRigCreate(self): def testRigCreate(self):
try:
# Requests address # Requests address
self.browser.get(self.live_server_url + '/event/create/') self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back # Gets redirected to login and back
@@ -230,6 +233,13 @@ class EventTest(LiveServerTestCase):
self.assertTrue(save.is_displayed()) self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_tag_name('form')
# For now, just check that HTML5 Client validation is in place TODO Test needs rewriting to properly test all levels of validation.
self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None)
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
# Create new person # Create new person
wait.until(animation_is_finished()) wait.until(animation_is_finished())
add_person_button = self.browser.find_element_by_xpath( add_person_button = self.browser.find_element_by_xpath(
@@ -287,7 +297,7 @@ class EventTest(LiveServerTestCase):
'//button[@data-id="id_person"]') '//button[@data-id="id_person"]')
person_select.send_keys(person1.name) person_select.send_keys(person1.name)
person_dropped = form.find_element_by_xpath( person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name) '//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person1.name)
person_dropped.click() person_dropped.click()
self.assertEqual(person1.name, form.find_element_by_xpath( self.assertEqual(person1.name, form.find_element_by_xpath(
@@ -382,7 +392,7 @@ class EventTest(LiveServerTestCase):
modal = self.browser.find_element_by_id("itemModal") 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_name").send_keys("Test Item 1")
modal.find_element_by_id("item_description").send_keys( modal.find_element_by_id("item_description").send_keys(
"This is an item description\nthat for reasons unkown spans two lines") "This is an item description\nthat for reasons unknown spans two lines")
e = modal.find_element_by_id("item_quantity") e = modal.find_element_by_id("item_quantity")
e.click() e.click()
e.send_keys(Keys.UP) e.send_keys(Keys.UP)
@@ -413,49 +423,25 @@ class EventTest(LiveServerTestCase):
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text) self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text) self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# Attempt to save - missing title save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
save.click() save.click()
# See error # TODO Testing of requirement for contact details
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 BaseException:
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)
# TODO Something seems broken with the CI tests here.
# See redirected to success page # See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text # successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name') # event = models.Event.objects.get(name='Test Event Name')
# self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
except WebDriverException:
# This is a dirty workaround for wercker being a bit funny and not running it correctly.
# Waiting for wercker to get back to me about this
pass
def testEventDuplicate(self): def testEventDuplicate(self):
client = models.Person.objects.create(name='Duplicate Test Person', email='duplicate@functional.test')
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
start_date=date.today() + timedelta(days=6), start_date=date.today() + timedelta(days=6),
description="start future no end", description="start future no end",
purchase_order='TESTPO', purchase_order='TESTPO',
person=client,
auth_request_by=self.profile, auth_request_by=self.profile,
auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00), auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00),
auth_request_to="some@email.address") auth_request_to="some@email.address")
@@ -499,7 +485,7 @@ class EventTest(LiveServerTestCase):
modal = self.browser.find_element_by_id("itemModal") modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 3") modal.find_element_by_id("item_name").send_keys("Test Item 3")
modal.find_element_by_id("item_description").send_keys( modal.find_element_by_id("item_description").send_keys(
"This is an item description\nthat for reasons unkown spans two lines") "This is an item description\nthat for reasons unknown spans two lines")
e = modal.find_element_by_id("item_quantity") e = modal.find_element_by_id("item_quantity")
e.click() e.click()
e.send_keys(Keys.UP) e.send_keys(Keys.UP)
@@ -572,6 +558,15 @@ class EventTest(LiveServerTestCase):
e = self.browser.find_element_by_id('id_name') e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name') e.send_keys('Test Event Name')
# Set person
person = models.Person.objects.create(name='Date Validation Person', email='datevalidation@functional.test')
person_select = form.find_element_by_xpath(
'//button[@data-id="id_person"]')
person_select.send_keys(person.name)
person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person.name)
person_dropped.click()
# Both dates, no times, end before start # Both dates, no times, end before start
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -677,6 +672,15 @@ class EventTest(LiveServerTestCase):
e = self.browser.find_element_by_id('id_name') e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name') e.send_keys('Test Event Name')
# Set person
person = models.Person.objects.create(name='Rig Non-Rig Person', email='rignonrig@functional.test')
person_select = form.find_element_by_xpath(
'//button[@data-id="id_person"]')
person_select.send_keys(person.name)
person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person.name)
person_dropped.click()
# Set an arbitrary date # Set an arbitrary date
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -738,9 +742,9 @@ class EventTest(LiveServerTestCase):
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..') organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
def testEventEdit(self): def testEventEdit(self):
person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save() person = models.Person.objects.create(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123")
organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save() organisation = models.Organisation.objects.create(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456")
venue = models.Venue(name="Event Detail Venue").save() venue = models.Venue.objects.create(name="Event Detail Venue")
eventData = { eventData = {
'name': "Detail Test", 'name': "Detail Test",

View File

@@ -11,30 +11,25 @@
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
<h3> <h3><a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a></h3>
<a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a> <h4>
<small class="label label-default"> <span class="label label-default">
<strong>Category:</strong> <strong>Category:</strong>
{{ object.category }} {{ object.category }}
</small> </span>
&nbsp; &nbsp;
<small class="label label-{{ object.status.display_class|default:'default' }}"> <span class="label label-{{ object.status.display_class|default:'default' }}">
<strong>Status:</strong> <strong>Status:</strong>
{{ object.status }} {{ object.status }}
</small> </span>
</h3> </h4>
<br>
{% if object.serial_number %} {% if object.serial_number %}
<p> <dt>Serial Number: </dt>
<strong>Serial Number: </strong> <dd>{{ object.serial_number }}</dd>
{{ object.serial_number }}
</p>
{% endif %} {% endif %}
{% if object.comments %} {% if object.comments %}
<p> <dt>Comments: </dt>
<strong>Comments: </strong> <dd class="dont-break-out">{{ object.comments|linebreaksbr }}<dd>
{{ object.comments|linebreaksbr }}
</p>
{% endif %} {% endif %}
</table> </table>