Compare commits

...

40 Commits

Author SHA1 Message Date
3e114484fe Merge branch 'master' into assets-testing 2020-02-08 13:12:27 +00:00
dependabot[bot]
116c497590 Bump pillow from 5.1.0 to 6.2.0 (#371)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.1.0 to 6.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/5.1.0...6.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Arona Jones <aj@aronajones.com>
2020-02-08 13:09:09 +00:00
f6f3149036 Merge pull request #397 from nottinghamtec/imgbot
[ImgBot] Optimize images
2020-02-08 13:00:00 +00:00
ea696deb1e Merge branch master into assets-testing
# Conflicts:
#	RIGS/test_functional.py
2020-02-07 22:18:21 +00:00
b8a4a70133 Fix broken search test 2020-02-07 21:57:32 +00:00
3028c053d4 Test visibility of form errors
And fix supplier form not displaying errors correctly!
2020-02-07 21:53:15 +00:00
3e16075b34 404 tests and test that everything requires authentication 2020-02-07 21:36:08 +00:00
762af5a1eb Asset search tests 2020-02-07 20:43:29 +00:00
d312634f68 Asset duplicate tests
Also fixed some random bugs
2020-02-07 20:00:11 +00:00
ca553137b6 Add supplier create/edit tests 2020-02-07 18:50:12 +00:00
1c577cdbee Helps if I add the migration... 2020-02-07 17:37:21 +00:00
eaeb56f9f9 Add supplier list page + tests
Also fix the supplier page not being ordered alphabetically
2020-02-07 17:35:16 +00:00
b8d9d24e2c Add delete sample data test for completeness
Chasing that sweet 100% coverage...
2020-02-07 00:17:28 +00:00
731f9865c6 Delete unused code
Much less effort way to increase coverage stats :D
2020-02-07 00:03:14 +00:00
be9d4115d0 Add access level tests 2020-02-06 23:18:08 +00:00
c6c8501640 Asset edit tests are here
A bit dodgy in places but par for the course for me :P
2020-02-06 22:33:26 +00:00
2641e80832 Basic asset create validation testing 2020-02-06 18:38:52 +00:00
7c30522906 Add cable asset creation test 2020-02-06 13:29:42 +00:00
b93bb29777 CHORE: Frickin pep8... 2020-02-05 20:32:21 +00:00
bf6d22bc38 Add non-cable asset creation test 2020-02-05 20:28:51 +00:00
Matthew Smith
5265ef835e Added Asset Create and Edit forms 2020-02-05 15:46:42 +00:00
39b22690f1 CHORE: pep8... 2020-02-05 13:33:14 +00:00
2a99e0912b FEAT: Adapt unit tests from RIGS to assets 2020-02-05 13:21:29 +00:00
Matthew Smith
a25c41150e Started POM and assets test 2020-01-23 18:29:18 +00:00
ImgBotApp
81e7bf6d46 [ImgBot] Optimize images
*Total -- 171.82kb -> 169.55kb (1.32%)

/RIGS/static/fonts/glyphicons-halflings-regular.svg -- 106.19kb -> 103.92kb (2.14%)
/RIGS/static/imgs/paperwork/corner-tr-su.jpg -- 65.63kb -> 65.63kb (0.01%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2020-01-21 16:46:49 +00:00
79f97bb05f Merge pull request #395 from nottinghamtec/misc
Miscellaneous changes/fixes
2020-01-21 16:40:00 +00:00
6ba87b0a5a CHANGE: Restructure the asset embed a bit
Should fix the forum mangling
2020-01-21 01:45:59 +00:00
1e03b5107e FIX: Rig Creation Test breaking on CI
Squashed commit messages below:
Also...
FIX: Person selection workflow in tests now works
FIX: Properly test name requirement in rig creation
And removed the dirty workaround for wercker so that the test no longer passes when it shouldn't. Which led to this mess of attempted fixes, now squashed:

Fine. Hardball.
What about if we click the button a different way...
Disable whole chunk of the test that was previously getting skipped
Perhaps you'd like to pass now Travis
Temporarily disable the offending part of the test.

Something odd is going on...TBC.
Reorder some selenium commands to try and fix test only failing on CI.
Another attempt at a CI test fix
FIX: Should fix rig create test failing on CI
2020-01-21 01:45:59 +00:00
39dbdd7ce4 FIX: Prevent long text breaking out of desc/notes
Just for good measure
2020-01-17 17:32:16 +00:00
1a953073be Merge branch 'master' into misc 2020-01-17 15:36:24 +00:00
630011aff7 FEAT: Add oembed for assets (#393)
* FEAT: Add oembed for assets

Don't see the worth in doing supplier currently...we don't OEmbed Org/Venue etc after all...

* FIX Copy paste error ;D

* Fix embeds not actually working for unauthenticated users

This is why I should have written tests...
2020-01-17 15:28:29 +00:00
Matthew Smith
e0c6a56263 Disable password reset as temporary fix to vulnerability (#396)
Disabled password reset and left message notifying user of problem. In response to CVE-2019-19844
2020-01-17 13:13:16 +00:00
87d460c799 FIX: Prevent long text breaking out of changelog popover
Might even be nicely hypenated on some platforms...

Closes #259
2020-01-14 19:38:26 +00:00
295397b32d CHANGE: Prevent both person and org being left blank for a Rig
Of course, there's no requirement that either of those have any actual contact details...

Closes #276.
2020-01-14 18:10:37 +00:00
10add5ab33 CHANGE: New SU Branding
Odds on it becoming outdated in less time than it took us to change it...

Closes #278.
2020-01-14 14:59:48 +00:00
7e3e8f37e2 FIX: Do not display auth warnings when duplicating events
Closes #326.
2020-01-13 22:13:37 +00:00
3a25b85e95 FEAT: Add conditional formatting to whole auth panel
Matches the formatting on the button
2020-01-13 21:07:09 +00:00
16b950c3b2 FIX: Make 'authorised at' datetime formatting match all the others
Closes #385
2020-01-13 21:07:09 +00:00
f616017423 CHANGE: Remove phone number field from initial registration
Closes #354
2020-01-13 21:07:09 +00:00
1480ae17fa CHORE: Update README.md. It was about time. 2020-01-13 20:24:16 +00:00
49 changed files with 1468 additions and 970 deletions

View File

@@ -6,6 +6,34 @@ from django.urls import reverse
from RIGS import models
def get_oembed(login_url, request, oembed_view, kwargs):
context = {}
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context)
return resp
def has_oembed(oembed_view, login_url=None):
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated:
return view_func(request, *args, **kwargs)
else:
if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
return _dec
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
"""
Decorator for views that checks that the user passes the given test.
@@ -25,11 +53,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
return view_func(request, *args, **kwargs)
elif not request.user.is_authenticated:
if oembed_view is not None:
context = {}
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context)
return resp
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
else:

36
PyRIGS/tests/base.py Normal file
View File

@@ -0,0 +1,36 @@
from django.test import LiveServerTestCase
from selenium import webdriver
from RIGS import models as rigsmodels
from . import pages
import os
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
if os.environ.get('CI', False):
options.add_argument("--headless")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(chrome_options=options)
return driver
class BaseTest(LiveServerTestCase):
def setUp(self):
super().setUpClass()
self.driver = create_browser()
def tearDown(self):
super().tearDown()
self.driver.quit()
class AutoLoginTest(BaseTest):
def setUp(self):
super().setUp()
self.profile = rigsmodels.Profile(
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
loginPage.login("EventTest", "EventTestPassword")

86
PyRIGS/tests/pages.py Normal file
View File

@@ -0,0 +1,86 @@
from pypom import Page, Region
from selenium.webdriver.common.by import By
from selenium.webdriver import Chrome
from selenium.common.exceptions import NoSuchElementException
class BasePage(Page):
form_items = {}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __getattr__(self, name):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
return form_element.value
else:
return super().__getattribute__(name)
def __setattr__(self, name, value):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
form_element.set_value(value)
else:
self.__dict__[name] = value
class FormPage(BasePage):
_errors_selector = (By.CLASS_NAME, "alert-danger")
def remove_all_required(self):
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
@property
def errors(self):
try:
error_page = self.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
return None
class ErrorPage(Region):
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
class ErrorItem(Region):
_field_selector = (By.CSS_SELECTOR, "dt")
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
@property
def field_name(self):
return self.find_element(*self._field_selector).text
@property
def errors(self):
return [x.text for x in self.find_elements(*self._error_selector)]
@property
def errors(self):
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
errors = {}
for error in error_items:
errors[error.field_name] = error.errors
return errors
class LoginPage(BasePage):
URL_TEMPLATE = '/user/login'
_username_locator = (By.ID, 'id_username')
_password_locator = (By.ID, 'id_password')
_submit_locator = (By.ID, 'id_submit')
_error_locator = (By.CSS_SELECTOR, '.errorlist>li')
def login(self, username, password):
username_element = self.find_element(*self._username_locator)
username_element.clear()
username_element.send_keys(username)
password_element = self.find_element(*self._password_locator)
password_element.clear()
password_element.send_keys(password)
self.find_element(*self._submit_locator).click()

133
PyRIGS/tests/regions.py Normal file
View File

@@ -0,0 +1,133 @@
from pypom import Region
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.select import Select
import datetime
def parse_bool_from_string(string):
# Used to convert from attribute strings to boolean values, written after I found this:
# >>> bool("false")
# True
if string == "true":
return True
else:
return False
class BootstrapSelectElement(Region):
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
_option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a[role=option]')
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
_status_locator = (By.CLASS_NAME, 'status')
@property
def is_open(self):
return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded"))
def toggle(self):
original_state = self.is_open
return self.find_element(*self._main_button_locator).click()
option_box = self.find_element(*self._option_box_locator)
if original_state:
self.wait.until(expected_conditions.invisibility_of_element_located(option_box))
else:
self.wait.until(expected_conditions.visibility_of_element_located(option_box))
def open(self):
if not self.is_open:
self.toggle()
def close(self):
if self.is_open:
self.toggle()
def select_all(self):
self.find_element(*self._select_all_locator).click()
def deselect_all(self):
self.find_element(*self._deselect_all_locator).click()
def search(self, query):
search_box = self.find_element(*self._search_locator)
search_box.clear()
search_box.send_keys(query)
status_text = self.find_element(*self._status_locator)
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
@property
def options(self):
options = list(self.find_elements(*self._option_locator))
return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected):
options = list((x for x in self.options if x.name == name))
assert len(options) == 1
options[0].set_selected(selected)
class BootstrapSelectOption(Region):
_text_locator = (By.CLASS_NAME, 'text')
@property
def selected(self):
return parse_bool_from_string(self.root.get_attribute("aria-selected"))
def toggle(self):
self.root.click()
def set_selected(self, selected):
if self.selected != selected:
self.toggle()
@property
def name(self):
return self.find_element(*self._text_locator).text
class TextBox(Region):
@property
def value(self):
return self.root.get_attribute("value")
def set_value(self, value):
self.root.clear()
self.root.send_keys(value)
class CheckBox(Region):
def toggle(self):
self.root.click()
@property
def value(self):
return parse_bool_from_string(self.root.get_attribute("checked"))
def set_value(self, value):
if value != self.value:
self.toggle()
class DatePicker(Region):
@property
def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d")
def set_value(self, value):
self.root.clear()
self.root.send_keys(value.strftime("%d%m%Y"))
class SingleSelectPicker(Region):
@property
def value(self):
picker = Select(self.root)
return picker.first_selected_option.text
def set_value(self, value):
picker = Select(self.root)
picker.select_by_visible_text(value)

View File

@@ -1,20 +1,13 @@
# TEC PA & Lighting - 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)
[![Dependency Status](https://gemnasium.com/badges/github.com/nottinghamtec/PyRIGS.svg)](https://gemnasium.com/github.com/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/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.
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? ###
For the rapid development of the application for medium term deployment, the main branch is being used.
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.
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.
@@ -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.
### 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.
@@ -115,5 +108,4 @@ python manage.py test RIGS.test_models.EventTestCase.test_current_events
```
### Committing, pushing and testing ###
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.
[![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)

View File

@@ -22,7 +22,7 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
class Meta:
model = models.Profile
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
fields = ('username', 'email', 'first_name', 'last_name', 'initials')
def clean_initials(self):
"""
@@ -129,6 +129,11 @@ class EventForm(forms.ModelForm):
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):
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.last_name = form.data['last_name']
user.initials = form.data['initials']
user.phone = form.data['phone']
# user.phone = form.data['phone']
user.save()

View File

@@ -140,15 +140,18 @@ class EventUpdate(generic.UpdateView):
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
# If this event has already been emailed to a client, show a warning
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.')
if hasattr(self.object, 'authorised'):
messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
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 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.')
if hasattr(self.object, 'authorised'):
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)
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 104 KiB

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;
}
.dont-break-out {
overflow-wrap: break-word;
word-wrap: break-word;
-webkit-hyphens: auto;
-ms-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
.modal-dialog {
z-index: inherit; // bug fix introduced in 52682ce
}

View File

@@ -122,7 +122,7 @@
<dd>&nbsp;</dd>
<dt>Event Description</dt>
<dd>{{ event.description|linebreaksbr }}</dd>
<dd class="dont-break-out">{{ event.description|linebreaksbr }}</dd>
<dd>&nbsp;</dd>
@@ -158,7 +158,15 @@
</div>
{% if event.is_rig and event.internal %}
<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-body">
<dl class="dl-horizontal col-sm-6">
@@ -188,7 +196,7 @@
</dd>
<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>
<dd>
@@ -216,7 +224,7 @@
<div class="panel-body">
<div class="well well-sm">
<h4>Notes</h4>
{{ event.notes|linebreaksbr }}
<div class="dont-break-out">{{ event.notes|linebreaksbr }}</div>
</div>
{% include 'RIGS/item_table.html' %}
</div>

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% block title %}Organisation | {{ object.name }}{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends 'base_rigs.html' %}
{% block title %}Password Reset Disabled{% endblock %}
{% block content %}
<h1>Password reset is disabled</h1>
<p> We are very sorry for the inconvenience, but due to a security vulnerability, password reset is currently disabled until the vulnerability can be patched.</p>
<p> If you are locked out of your account, please contact an administrator and we can manually perform a reset</p>
{% endblock %}

View File

@@ -14,7 +14,7 @@
{% for change in itemChange.field_changes %}
<li class="list-group-item">
<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>
{% endfor %}
</ul>
@@ -23,4 +23,4 @@
{% endfor %}
{% else %}
nothing useful
{% endif %}
{% endif %}

View File

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

View File

@@ -1,17 +1,20 @@
# -*- coding: utf-8 -*-
import os
import re
import pytz
from datetime import date, time, datetime, timedelta
from django.core import mail
import pytz
from django.conf import settings
from django.core import mail, signing
from django.db import transaction
from django.http import HttpResponseBadRequest
from django.test import LiveServerTestCase, TestCase
from django.test.client import Client
from django.urls import reverse
from reversion import revisions as reversion
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.support.ui import WebDriverWait
@@ -20,23 +23,12 @@ from RIGS import models
from reversion import revisions as reversion
from django.urls import reverse
from django.core import mail, signing
from PyRIGS.tests.base import create_browser
from django.conf import settings
import sys
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
if os.environ.get('CI', False):
options.add_argument("--headless")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(chrome_options=options)
return driver
class UserRegistrationTest(LiveServerTestCase):
def setUp(self):
self.browser = create_browser()
@@ -74,8 +66,9 @@ class UserRegistrationTest(LiveServerTestCase):
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')
# No longer required for new users
# 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')
@@ -86,7 +79,7 @@ class UserRegistrationTest(LiveServerTestCase):
first_name.send_keys('John')
last_name.send_keys('Smith')
initials.send_keys('JS')
phone.send_keys('0123456789')
# phone.send_keys('0123456789')
self.browser.execute_script(
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
@@ -164,7 +157,7 @@ class UserRegistrationTest(LiveServerTestCase):
self.assertEqual(profileObject.first_name, 'John')
self.assertEqual(profileObject.last_name, 'Smith')
self.assertEqual(profileObject.initials, 'JS')
self.assertEqual(profileObject.phone, '0123456789')
# self.assertEqual(profileObject.phone, '0123456789')
self.assertEqual(profileObject.email, 'test@example.com')
# All is well
@@ -219,254 +212,236 @@ class EventTest(LiveServerTestCase):
self.browser.get(self.live_server_url + '/rigboard/')
def testRigCreate(self):
try:
# Requests address
self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back
self.authenticate('/event/create/')
# 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, 3) # setup WebDriverWait to use later (to wait for animations)
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
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())
# 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()
# 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')
# Slider expands and save button visible
self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form')
# Create new person
wait.until(animation_is_finished())
add_person_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "add")]')
add_person_button.click()
# 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)
# 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)
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
# 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())
# Create new person
wait.until(animation_is_finished())
add_person_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "add")]')
add_person_button.click()
# See 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")))
# 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)
# Change mind and add another
wait.until(animation_is_finished())
add_person_button.click()
# 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())
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
# 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")))
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())
# Change mind and add another
wait.until(animation_is_finished())
add_person_button.click()
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")))
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
# 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()
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())
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")))
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")))
# 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)
# 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, "dropdown-menu")]//span[contains(text(), "%s")]' % person1.name)
person_dropped.click()
wait.until(animation_is_finished())
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")))
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)
# 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)
# Create organisation
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
# 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")))
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 venue
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_venue" and contains(@href, "add")]')
wait.until(animation_is_finished())
add_button.click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# Create organisation
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.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")))
# 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")))
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
form.find_element_by_id('id_start_time').send_keys('06:59')
# Create venue
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_venue" and contains(@href, "add")]')
wait.until(animation_is_finished())
add_button.click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
form.find_element_by_id('id_end_time').send_keys('07:00')
# 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")))
# 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
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
form.find_element_by_id('id_start_time').send_keys('06:59')
# 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
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
form.find_element_by_id('id_end_time').send_keys('07:00')
# 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('£ 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('£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# 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 unknown 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
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# 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
# Attempt to save - missing title
save.click()
# 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('£ 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('£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# 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 BaseException:
self.assertFail("Element does not appear to have been deleted")
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# 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")))
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
save.click()
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
e.send_keys(Keys.ENTER)
# TODO Testing of requirement for contact details
# See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name')
self.assertIn("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
# TODO Something seems broken with the CI tests here.
# See redirected to success page
# successTitle = self.browser.find_element_by_xpath('//h1').text
# event = models.Event.objects.get(name='Test Event Name')
# self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
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,
start_date=date.today() + timedelta(days=6),
description="start future no end",
purchase_order='TESTPO',
person=client,
auth_request_by=self.profile,
auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00),
auth_request_to="some@email.address")
@@ -510,7 +485,7 @@ class EventTest(LiveServerTestCase):
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_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.click()
e.send_keys(Keys.UP)
@@ -583,6 +558,15 @@ class EventTest(LiveServerTestCase):
e = self.browser.find_element_by_id('id_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
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -688,6 +672,15 @@ class EventTest(LiveServerTestCase):
e = self.browser.find_element_by_id('id_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
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -749,9 +742,9 @@ class EventTest(LiveServerTestCase):
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
def testEventEdit(self):
person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save()
organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
venue = models.Venue(name="Event Detail Venue").save()
person = models.Person.objects.create(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123")
organisation = models.Organisation.objects.create(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456")
venue = models.Venue.objects.create(name="Event Detail Venue")
eventData = {
'name': "Detail Test",

View File

@@ -1,5 +1,3 @@
import pytz
from reversion import revisions as reversion
from django.conf import settings
@@ -8,6 +6,7 @@ from django.test import TestCase
from RIGS import models, versioning
from datetime import date, timedelta, datetime, time
from decimal import *
from PyRIGS.tests.base import create_browser
class ProfileTestCase(TestCase):

View File

@@ -19,7 +19,7 @@ urlpatterns = [
url('^user/login/$', views.login, name='login'),
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}),
url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()),
# People
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),

View File

@@ -392,3 +392,7 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save()
return reverse_lazy('profile_detail')
class PasswordResetDisabled(generic.TemplateView):
template_name = "RIGS/password_reset_disable.html"

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class AssetsConfig(AppConfig):
name = 'assets'

View File

@@ -1,9 +0,0 @@
import django_filters
from assets import models
class AssetFilter(django_filters.FilterSet):
class Meta:
model = models.Asset
fields = ['asset_id', 'description', 'serial_number', 'category', 'status']

View File

@@ -1,229 +0,0 @@
import os
import datetime
import xml.etree.ElementTree as ET
from django.core.management.base import BaseCommand
from django.conf import settings
from assets import models
class Command(BaseCommand):
help = 'Imports old db from XML dump'
epoch = datetime.date(1970, 1, 1)
def handle(self, *args, **options):
self.import_categories()
self.import_statuses()
self.import_suppliers()
self.import_collections()
self.import_assets()
self.import_cables()
@staticmethod
def xml_path(file):
return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
@staticmethod
def parse_xml(file):
tree = ET.parse(file)
return tree.getroot()
def import_categories(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Asset_Categories.xml'))
for child in root:
obj, created = models.AssetCategory.objects.update_or_create(
pk=int(child.find('AssetCategoryID').text),
name=child.find('AssetCategory').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_statuses(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Asset_Status_new.xml'))
for child in root:
obj, created = models.AssetStatus.objects.update_or_create(
pk=int(child.find('StatusID').text),
name=child.find('Status').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Statuses - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_suppliers(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Asset_Suppliers_new.xml'))
for child in root:
obj, created = models.Supplier.objects.update_or_create(
pk=int(child.find('Supplier_x0020_Id').text),
name=child.find('Supplier_x0020_Name').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Suppliers - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_assets(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Assets.xml'))
for child in root:
defaults = dict()
# defaults['pk'] = int(child.find('ID').text)
defaults['asset_id'] = child.find('AssetID').text
try:
defaults['description'] = child.find('AssetDescription').text
except AttributeError:
defaults['description'] = 'None'
defaults['category'] = models.AssetCategory.objects.get(pk=int(child.find('AssetCategoryID').text))
defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('StatusID').text))
try:
defaults['serial_number'] = child.find('SerialNumber').text
except AttributeError:
pass
try:
defaults['purchased_from'] = models.Supplier.objects.get(pk=int(child.find('Supplier_x0020_Id').text))
except AttributeError:
pass
try:
defaults['date_acquired'] = datetime.datetime.strptime(child.find('DateAcquired').text, '%d/%m/%Y').date()
except AttributeError:
defaults['date_acquired'] = self.epoch
try:
defaults['date_sold'] = datetime.datetime.strptime(child.find('DateSold').text, '%d/%m/%Y').date()
except AttributeError:
pass
try:
defaults['purchase_price'] = float(child.find('Replacement_x0020_Value').text)
except AttributeError:
pass
try:
defaults['salvage_value'] = float(child.find('SalvageValue').text)
except AttributeError:
pass
try:
defaults['comments'] = child.find('Comments').text
except AttributeError:
pass
try:
date = child.find('NextSchedMaint').text.split('T')[0]
defaults['next_sched_maint'] = datetime.datetime.strptime(date, '%Y-%m-%d').date()
except AttributeError:
pass
print(defaults)
obj, created = models.Asset.objects.update_or_create(**defaults)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Assets - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_collections(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Cable_Collections.xml'))
for child in root:
defaults = dict()
defaults['pk'] = int(child.find('ID').text)
defaults['name'] = child.find('Cable_x0020_Trunk').text
obj, created = models.Collection.objects.update_or_create(**defaults)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_cables(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Cables.xml'))
for child in root:
defaults = dict()
defaults['asset_id'] = child.find('Asset_x0020_Number').text
try:
defaults['description'] = child.find('Type_x0020_of_x0020_Cable').text
except AttributeError:
defaults['description'] = 'None'
defaults['is_cable'] = True
defaults['category'] = models.AssetCategory.objects.get(pk=9)
try:
defaults['length'] = child.find('Length_x0020__x0028_m_x0029_').text
except AttributeError:
pass
defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('Status').text))
try:
defaults['comments'] = child.find('Comments').text
except AttributeError:
pass
try:
collection_id = int(child.find('Collection').text)
if collection_id != 0:
defaults['collection'] = models.Collection.objects.get(pk=collection_id)
except AttributeError:
pass
try:
defaults['purchase_price'] = float(child.find('Purchase_x0020_Price').text)
except AttributeError:
pass
defaults['date_acquired'] = self.epoch
print(defaults)
obj, created = models.Asset.objects.update_or_create(**defaults)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))

View File

@@ -1,110 +0,0 @@
import os
import datetime
import xml.etree.ElementTree as ET
from django.core.management.base import BaseCommand
from django.conf import settings
class Command(BaseCommand):
help = 'Imports old db from XML dump'
epoch = datetime.date(1970, 1, 1)
def handle(self, *args, **options):
# self.update_statuses()
# self.update_suppliers()
self.update_cable_statuses()
@staticmethod
def xml_path(file):
return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
@staticmethod
def parse_xml(file):
tree = ET.parse(file)
return tree.getroot()
def update_statuses(self):
file = self.xml_path('TEC_Assets.xml')
tree = ET.parse(file)
root = tree.getroot()
# map old status pk to new status pk
status_map = {
2: 2,
3: 4,
4: 3,
5: 5,
6: 1
}
for child in root:
status = int(child.find('StatusID').text)
child.find('StatusID').text = str(status_map[status])
tree.write(file)
def update_suppliers(self):
old_file = self.xml_path('TEC_Asset_Suppliers.xml')
old_tree = ET.parse(old_file)
old_root = old_tree.getroot()
new_file = self.xml_path('TEC_Asset_Suppliers_new.xml')
new_tree = ET.parse(new_file)
new_root = new_tree.getroot()
# map old supplier pk to new supplier pk
supplier_map = dict()
def find_in_old(name, root):
for child in root:
found_id = child.find('Supplier_x0020_Id').text
found_name = child.find('Supplier_x0020_Name').text
if found_name == name:
return found_id
for new_child in new_root:
new_id = new_child.find('Supplier_x0020_Id').text
new_name = new_child.find('Supplier_x0020_Name').text
old_id = find_in_old(new_name, old_root)
supplier_map[int(old_id)] = int(new_id)
file = self.xml_path('TEC_Assets.xml')
tree = ET.parse(file)
root = tree.getroot()
for child in root:
try:
supplier = int(child.find('Supplier_x0020_Id').text)
child.find('Supplier_x0020_Id').text = str(supplier_map[supplier])
except AttributeError:
pass
tree.write(file)
def update_cable_statuses(self):
file = self.xml_path('TEC_Cables.xml')
tree = ET.parse(file)
root = tree.getroot()
# map old status pk to new status pk
status_map = {
0: 7,
1: 3,
3: 2,
4: 5,
6: 6,
7: 1,
8: 4,
9: 2,
}
for child in root:
status = int(child.find('Status').text)
child.find('Status').text = str(status_map[status])
tree.write(file)

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.0.13 on 2020-02-07 17:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200103_2215'),
]
operations = [
migrations.AlterModelOptions(
name='supplier',
options={'ordering': ['name'], 'permissions': (('view_supplier', 'Can view a supplier'),)},
),
]

View File

@@ -44,6 +44,7 @@ class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
class Meta:
ordering = ['name']
permissions = (
('view_supplier', 'Can view a supplier'),
)

View File

@@ -1,6 +1,5 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %}

View File

@@ -0,0 +1,40 @@
{% extends 'base_embed.html' %}
{% load static from staticfiles %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<a href="/assets">
<span class="source"> TEC Asset Database</span>
</a>
</div>
<div class="col-sm-12">
<h3><a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a></h3>
<h4>
<span class="label label-default">
<strong>Category:</strong>
{{ object.category }}
</span>
&nbsp;
<span class="label label-{{ object.status.display_class|default:'default' }}">
<strong>Status:</strong>
{{ object.status }}
</span>
</h4>
{% if object.serial_number %}
<dt>Serial Number: </dt>
<dd>{{ object.serial_number }}</dd>
{% endif %}
{% if object.comments %}
<dt>Comments: </dt>
<dd class="dont-break-out">{{ object.comments|linebreaksbr }}<dd>
{% endif %}
</table>
</div>
</div>
{% endblock %}

View File

@@ -17,16 +17,16 @@
</div>
<br>
<div style="margin-top: 1em;" class="pull-right">
<div class="form-group">
<div id="category-group" class="form-group">
<label for="category" class="sr-only">Category</label>
{% render_field form.category|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
</div>
<div class="form-group">
<div id="status-group" class="form-group">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
<!---TODO: Auto filter whenever an option is selected, instead of using a button -->
<button type="submit" class="btn btn-default">Filter</button>
<button id="filter-submit" type="submit" class="btn btn-default">Filter</button>
</div>
</form>

View File

@@ -1,6 +1,5 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %}
@@ -40,7 +39,6 @@
<div class="row">
<div class="col-md-12">
{% include 'partials/asset_buttons.html' %}
</div>
</div>
</form>

View File

@@ -4,7 +4,7 @@
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
{% elif duplicate %}
<!--duplicate-->
<button type="submit" class="btn btn-default"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
{% elif create %}
<!--create-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>

View File

@@ -1,5 +1,4 @@
{% load widget_tweaks %}
{% load asset_templatetags %}
<div class="panel panel-default">
<div class="panel-heading">
Asset Details

View File

@@ -1,10 +1,11 @@
{% for item in object_list %}
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for--> <tr class={{ item.status.display_class|default:"" }}>
<td style="vertical-align: middle;"><a href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
<td style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
<td style="vertical-align: middle;">{{ item.category }}</td>
<td style="vertical-align: middle;">{{ item.status }}</td>
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for-->
<tr class="{{ item.status.display_class|default:'' }} assetRow">
<td style="vertical-align: middle;"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
<td class="assetDesc" style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
<td class="assetCategory" style="vertical-align: middle;">{{ item.category }}</td>
<td class="assetStatus" style="vertical-align: middle;">{{ item.status }}</td>
<td class="hidden-xs">
<div class="btn-group" role="group">
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.asset_id %}"><i class="glyphicon glyphicon-eye-open"></i> View</a>

View File

@@ -1,5 +1,4 @@
{% load widget_tweaks %}
{% load asset_templatetags %}
<div class="panel panel-default">
<div class="panel-heading">
Cable Details

View File

@@ -1,12 +1,11 @@
{% load widget_tweaks %}
{% load asset_templatetags %}
<div class="panel panel-default">
<div class="panel-heading">
Collection Details
</div>
<div class="panel-body">
{% if create or edit or duplicate %}
<div class="form-group">
<div class="form-group" id="parent-group">
<label for="selectpicker">Set Parent</label>
{% include 'partials/asset_picker.html' %}
</div>

View File

@@ -1,5 +1,4 @@
{% load widget_tweaks %}
{% load asset_templatetags %}
{% load static %}
@@ -23,7 +22,7 @@
</div>
<div class="panel-body">
{% if create or edit or duplicate %}
<div class="form-group">
<div class="form-group" id="purchased-from-group">
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
{% if object.purchased_from %}

View File

@@ -14,7 +14,7 @@
<div class="input-group pull-right" style="width: auto;">
{% render_field form.query|add_class:'form-control' placeholder='Search by Name' style="width: 250px"%}
<label for="query" class="sr-only">Name:</label>
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
<span class="input-group-btn"><button type="submit" class="btn btn-default" id="id_search">Search</button></span>
</div>
</form>
@@ -27,8 +27,8 @@
</thead>
<tbody id="asset_table_body">
{% for item in object_list %}
<tr>
<td>{{ item.name }}</td>
<tr class="supplierRow">
<td class="supplierName">{{ item.name }}</td>
<td>
<a href="{% url 'supplier_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
<a href="{% url 'supplier_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>

View File

@@ -13,8 +13,8 @@
</div>
<form method="post">
{% csrf_token %}
{{ form }}
{% include 'form_errors.html' %}
{{ form }}
<input type="submit" value="Save" class="btn btn-success">
</form>
{% endblock %}

View File

@@ -1,21 +0,0 @@
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.safestring import SafeData, mark_safe
from django.utils.text import normalize_newlines
from django.utils.html import escape
register = template.Library()
@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
def linebreaksn(value, autoescape=True):
"""
Convert all newlines in a piece of plain text to jQuery line breaks
(`\n`).
"""
autoescape = autoescape and not isinstance(value, SafeData)
value = normalize_newlines(value)
if autoescape:
value = escape(value)
return mark_safe(value.replace('\n', '\\n'))

188
assets/tests/pages.py Normal file
View File

@@ -0,0 +1,188 @@
# Collection of page object models for use within tests.
from pypom import Page, Region
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver import Chrome
from django.urls import reverse
from PyRIGS.tests import regions
from PyRIGS.tests.pages import BasePage, FormPage
import pdb
class AssetList(BasePage):
URL_TEMPLATE = '/assets/asset/list'
_asset_item_locator = (By.CLASS_NAME, 'assetRow')
_search_text_locator = (By.ID, 'id_query')
_status_select_locator = (By.CSS_SELECTOR, 'div#status-group>div.bootstrap-select')
_category_select_locator = (By.CSS_SELECTOR, 'div#category-group>div.bootstrap-select')
_go_button_locator = (By.ID, 'filter-submit')
class AssetListRow(Region):
_asset_id_locator = (By.CLASS_NAME, "assetID")
_asset_description_locator = (By.CLASS_NAME, "assetDesc")
_asset_category_locator = (By.CLASS_NAME, "assetCategory")
_asset_status_locator = (By.CLASS_NAME, "assetStatus")
@property
def id(self):
return self.find_element(*self._asset_id_locator).text
@property
def description(self):
return self.find_element(*self._asset_description_locator).text
@property
def category(self):
return self.find_element(*self._asset_category_locator).text
@property
def status(self):
return self.find_element(*self._asset_status_locator).text
@property
def assets(self):
return [self.AssetListRow(self, i) for i in self.find_elements(*self._asset_item_locator)]
@property
def query(self):
return self.find_element(*self._search_text_locator).text
def set_query(self, queryString):
element = self.find_element(*self._search_text_locator)
element.clear()
element.send_keys(queryString)
def search(self):
self.find_element(*self._go_button_locator).click()
@property
def status_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._status_select_locator))
@property
def category_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._category_select_locator))
class AssetForm(FormPage):
_purchased_from_select_locator = (By.CSS_SELECTOR, 'div#purchased-from-group>div.bootstrap-select')
_parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select')
_submit_locator = (By.CLASS_NAME, 'btn-success')
form_items = {
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'comments': (regions.TextBox, (By.ID, 'id_comments')),
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
'plug': (regions.SingleSelectPicker, (By.ID, 'id_plug')),
'socket': (regions.SingleSelectPicker, (By.ID, 'id_socket')),
'length': (regions.TextBox, (By.ID, 'id_length')),
'csa': (regions.TextBox, (By.ID, 'id_csa')),
'circuits': (regions.TextBox, (By.ID, 'id_circuits')),
'cores': (regions.TextBox, (By.ID, 'id_cores'))
}
@property
def purchased_from_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._purchased_from_select_locator))
@property
def parent_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._parent_select_locator))
def submit(self):
previous_errors = self.errors
self.find_element(*self._submit_locator).click()
self.wait.until(lambda x: self.errors != previous_errors or self.success)
class AssetEdit(AssetForm):
URL_TEMPLATE = '/assets/asset/id/{asset_id}/edit/'
@property
def success(self):
return '/edit' not in self.driver.current_url
class AssetCreate(AssetForm):
URL_TEMPLATE = '/assets/asset/create/'
@property
def success(self):
return '/create' not in self.driver.current_url
class AssetDuplicate(AssetForm):
URL_TEMPLATE = '/assets/asset/id/{asset_id}/duplicate'
@property
def success(self):
return '/duplicate' not in self.driver.current_url
class SupplierList(BasePage):
URL_TEMPLATE = reverse('supplier_list')
_supplier_item_locator = (By.CLASS_NAME, 'supplierRow')
_search_text_locator = (By.ID, 'id_query')
_go_button_locator = (By.ID, 'id_search')
class SupplierListRow(Region):
_name_locator = (By.CLASS_NAME, "supplierName")
@property
def name(self):
return self.find_element(*self._name_locator).text
@property
def suppliers(self):
return [self.SupplierListRow(self, i) for i in self.find_elements(*self._supplier_item_locator)]
@property
def query(self):
return self.find_element(*self._search_text_locator).text
def set_query(self, queryString):
element = self.find_element(*self._search_text_locator)
element.clear()
element.send_keys(queryString)
def search(self):
self.find_element(*self._go_button_locator).click()
class SupplierForm(FormPage):
_submit_locator = (By.CLASS_NAME, 'btn-success')
form_items = {
'name': (regions.TextBox, (By.ID, 'id_name')),
}
def submit(self):
previous_errors = self.errors
self.find_element(*self._submit_locator).click()
self.wait.until(lambda x: self.errors != previous_errors or self.success)
class SupplierCreate(SupplierForm):
URL_TEMPLATE = reverse('supplier_create')
@property
def success(self):
return '/create' not in self.driver.current_url
class SupplierEdit(SupplierForm):
# TODO This should be using reverse
URL_TEMPLATE = '/assets/supplier/{supplier_id}/edit'
@property
def success(self):
return '/edit' not in self.driver.current_url

583
assets/tests/test_assets.py Normal file
View File

@@ -0,0 +1,583 @@
from . import pages
from django.core.management import call_command
from django.test import TestCase
from assets import models
from django.test.utils import override_settings
from django.urls import reverse
from urllib.parse import urlparse
from RIGS import models as rigsmodels
from PyRIGS.tests.base import BaseTest, AutoLoginTest
from assets import models, urls
from reversion import revisions as reversion
from selenium.webdriver.common.keys import Keys
import datetime
class TestAssetList(AutoLoginTest):
def setUp(self):
super().setUp()
sound = models.AssetCategory.objects.create(name="Sound")
lighting = models.AssetCategory.objects.create(name="Lighting")
working = models.AssetStatus.objects.create(name="Working", should_show=True)
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting, date_acquired=datetime.date(2020, 2, 1))
self.page = pages.AssetList(self.driver, self.live_server_url).open()
def test_default_statuses_applied(self):
# Only the working stuff should be shown initially
assetDescriptions = list(map(lambda x: x.description, self.page.assets))
self.assertEqual(2, len(assetDescriptions))
self.assertIn("A light", assetDescriptions)
self.assertIn("Working Mic", assetDescriptions)
def test_asset_order(self):
# Only the working stuff should be shown initially
self.page.status_selector.open()
self.page.status_selector.set_option("Broken", True)
self.page.status_selector.close()
self.page.search()
assetIDs = list(map(lambda x: x.id, self.page.assets))
self.assertEqual("1", assetIDs[0])
self.assertEqual("2", assetIDs[1])
self.assertEqual("10", assetIDs[2])
self.assertEqual("C1", assetIDs[3])
def test_search(self):
self.page.set_query("10")
self.page.search()
self.assertTrue(len(self.page.assets) == 1)
self.assertEqual("Working Mic", self.page.assets[0].description)
self.assertEqual("10", self.page.assets[0].id)
self.page.set_query("light")
self.page.search()
self.assertTrue(len(self.page.assets) == 1)
self.assertEqual("A light", self.page.assets[0].description)
self.page.set_query("Random string")
self.page.search()
self.assertTrue(len(self.page.assets) == 0)
self.page.set_query("")
self.page.search()
# Only working stuff shown by default
self.assertTrue(len(self.page.assets) == 2)
self.page.status_selector.toggle()
self.assertTrue(self.page.status_selector.is_open)
self.page.status_selector.select_all()
self.page.status_selector.toggle()
self.assertFalse(self.page.status_selector.is_open)
self.page.search()
self.assertTrue(len(self.page.assets) == 4)
self.page.category_selector.toggle()
self.assertTrue(self.page.category_selector.is_open)
self.page.category_selector.set_option("Sound", True)
self.page.category_selector.close()
self.assertFalse(self.page.category_selector.is_open)
self.page.search()
self.assertTrue(len(self.page.assets) == 2)
assetIDs = list(map(lambda x: x.id, self.page.assets))
self.assertEqual("1", assetIDs[0])
self.assertEqual("10", assetIDs[1])
class TestAssetForm(AutoLoginTest):
def setUp(self):
super().setUp()
self.category = models.AssetCategory.objects.create(name="Health & Safety")
self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True)
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status, category=self.category, date_acquired=datetime.date(2000, 1, 1))
self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240, num_pins=3)
self.page = pages.AssetCreate(self.driver, self.live_server_url).open()
def test_asset_create(self):
# Test that ID is automatically assigned and properly incremented
self.assertIn(self.page.asset_id, "9001")
self.page.remove_all_required()
self.page.asset_id = "XX$X"
self.page.submit()
self.assertFalse(self.page.success)
self.assertIn("An Asset ID can only consist of letters and numbers, with a final number", self.page.errors["Asset id"])
self.assertIn("This field is required.", self.page.errors["Description"])
self.page.open()
self.page.description = "Bodge Lead"
self.page.category = "Health & Safety"
self.page.status = "O.K."
self.page.serial_number = "0124567890-SAUSAGE"
self.page.comments = "This is actually a sledgehammer, not a cable..."
self.page.purchased_from_selector.toggle()
self.assertTrue(self.page.purchased_from_selector.is_open)
self.page.purchased_from_selector.search(self.supplier.name[:-8])
self.page.purchased_from_selector.set_option(self.supplier.name, True)
self.assertFalse(self.page.purchased_from_selector.is_open)
self.page.purchase_price = "12.99"
self.page.salvage_value = "99.12"
self.date_acquired = "05022020"
self.page.parent_selector.toggle()
self.assertTrue(self.page.parent_selector.is_open)
# Searching it by ID autoselects it
self.page.parent_selector.search(self.parent.asset_id)
# Needed here but not earlier for whatever reason
self.driver.implicitly_wait(1)
# self.page.parent_selector.set_option(self.parent.asset_id + " | " + self.parent.description, True)
# Need to explicitly close as we haven't selected anything to trigger the auto close
self.page.parent_selector.search(Keys.ESCAPE)
self.assertFalse(self.page.parent_selector.is_open)
self.assertTrue(self.page.parent_selector.options[0].selected)
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
self.page.submit()
self.assertTrue(self.page.success)
def test_cable_create(self):
self.page.description = "IEC -> IEC"
self.page.category = "Health & Safety"
self.page.status = "O.K."
self.page.serial_number = "MELON-MELON-MELON"
self.page.comments = "You might need that"
self.page.is_cable = True
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
self.page.plug = "IEC"
self.page.socket = "IEC"
self.page.length = 10
self.page.csa = "1.5"
self.page.circuits = 1
self.page.cores = 3
self.page.submit()
self.assertTrue(self.page.success)
def test_asset_edit(self):
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
new_description = "Big Shelf"
self.page.description = new_description
self.page.submit()
self.assertTrue(self.page.success)
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
def test_asset_duplicate(self):
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
self.assertEqual(self.parent.description, self.page.description)
self.assertEqual(self.parent.status.name, self.page.status)
self.assertEqual(self.parent.category.name, self.page.category)
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
self.page.submit()
self.assertTrue(self.page.success)
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
class TestSupplierList(AutoLoginTest):
def setUp(self):
super().setUp()
models.Supplier.objects.create(name="Fullmetal Heavy Industry")
models.Supplier.objects.create(name="Acme.")
models.Supplier.objects.create(name="TEC PA & Lighting")
models.Supplier.objects.create(name="Caterpillar Inc.")
models.Supplier.objects.create(name="N.E.R.D")
models.Supplier.objects.create(name="Khumalo")
models.Supplier.objects.create(name="1984 Incorporated")
self.page = pages.SupplierList(self.driver, self.live_server_url).open()
# Should be sorted alphabetically
def test_order(self):
names = list(map(lambda x: x.name, self.page.suppliers))
self.assertEqual("1984 Incorporated", names[0])
self.assertEqual("Acme.", names[1])
self.assertEqual("Caterpillar Inc.", names[2])
self.assertEqual("Fullmetal Heavy Industry", names[3])
self.assertEqual("Khumalo", names[4])
self.assertEqual("N.E.R.D", names[5])
self.assertEqual("TEC PA & Lighting", names[6])
def test_search(self):
self.page.set_query("TEC")
self.page.search()
self.assertTrue(len(self.page.suppliers) == 1)
self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name)
self.page.set_query("")
self.page.search()
self.assertTrue(len(self.page.suppliers) == 7)
self.page.set_query("This is not a supplier")
self.page.search()
self.assertTrue(len(self.page.suppliers) == 0)
class TestSupplierCreateAndEdit(AutoLoginTest):
def setUp(self):
super().setUp()
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
def test_supplier_create(self):
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
self.page.remove_all_required()
self.page.submit()
self.assertFalse(self.page.success)
self.assertIn("This field is required.", self.page.errors["Name"])
self.page.name = "Optican Health Supplies"
self.page.submit()
self.assertTrue(self.page.success)
def test_supplier_edit(self):
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
self.assertEquals("Fullmetal Heavy Industry", self.page.name)
new_name = "Cyberdyne Systems"
self.page.name = new_name
self.page.submit()
self.assertTrue(self.page.success)
class TestSupplierValidation(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = rigsmodels.Profile.objects.create(username="SupplierValidationTest", email="SVT@test.com", is_superuser=True, is_active=True, is_staff=True)
cls.supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test_create(self):
url = reverse('supplier_create')
response = self.client.post(url)
self.assertFormError(response, 'form', 'name', 'This field is required.')
def test_edit(self):
url = reverse('supplier_update', kwargs={'pk': self.supplier.pk})
response = self.client.post(url, {'name': ""})
self.assertFormError(response, 'form', 'name', 'This field is required.')
class Test404(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = rigsmodels.Profile.objects.create(username="404Test", email="404@test.com", is_superuser=True, is_active=True, is_staff=True)
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test(self):
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
for url_name in urls:
request_url = reverse(url_name, kwargs={'pk': "0000"})
response = self.client.get(request_url, follow=True)
self.assertEqual(response.status_code, 404)
# @tag('slow') TODO: req. Django 3.0
class TestAccessLevels(TestCase):
@override_settings(DEBUG=True)
def setUp(self):
super().setUp()
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production
call_command('generateSampleData')
# Nothing should be available to the unauthenticated
def test_unauthenticated(self):
for url in urls.urlpatterns:
if url.name is not None:
pattern = str(url.pattern)
if "json" in url.name or pattern:
# TODO
pass
elif ":pk>" in pattern:
request_url = reverse(url.name, kwargs={'pk': 9})
else:
request_url = reverse(url.name)
response = self.client.get(request_url, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 302)
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'login')
def test_basic_access(self):
self.assertTrue(self.client.login(username="basic", password="basic"))
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons not shown in list
self.assertNotContains(response, 'Edit')
self.assertNotContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': "9000"})
response = self.client.get(url)
self.assertNotContains(response, 'Purchase Details')
self.assertNotContains(response, 'View Revision History')
urls = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urls:
request_url = reverse(url_name, kwargs={'pk': "9000"})
response = self.client.get(request_url, follow=True)
self.assertEqual(response.status_code, 403)
request_url = reverse('supplier_create')
response = self.client.get(request_url, follow=True)
self.assertEqual(response.status_code, 403)
request_url = reverse('supplier_update', kwargs={'pk': "1"})
response = self.client.get(request_url, follow=True)
self.assertEqual(response.status_code, 403)
def test_keyholder_access(self):
self.assertTrue(self.client.login(username="keyholder", password="keyholder"))
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons shown in list
self.assertContains(response, 'Edit')
self.assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': "9000"})
response = self.client.get(url)
self.assertContains(response, 'Purchase Details')
self.assertContains(response, 'View Revision History')
# def test_finance_access(self): Level not used in assets currently
class TestFormValidation(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = rigsmodels.Profile.objects.create(username="AssetCreateValidationTest", email="acvt@test.com", is_superuser=True, is_active=True, is_staff=True)
cls.category = models.AssetCategory.objects.create(name="Sound")
cls.status = models.AssetStatus.objects.create(name="Broken", should_show=True)
cls.asset = models.Asset.objects.create(asset_id="9999", description="The Office", status=cls.status, category=cls.category, date_acquired=datetime.date(2018, 6, 15))
cls.connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, plug=cls.connector, socket=cls.connector, length=10, csa="1.5", circuits=1, cores=3)
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test_asset_create(self):
url = reverse('asset_create')
response = self.client.post(url, {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
self.assertFormError(response, 'form', 'description', 'This field is required.')
self.assertFormError(response, 'form', 'status', 'This field is required.')
self.assertFormError(response, 'form', 'category', 'This field is required.')
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
def test_cable_create(self):
url = reverse('asset_create')
response = self.client.post(url, {'asset_id': 'X$%A', 'is_cable': True})
self.assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
self.assertFormError(response, 'form', 'plug', 'A cable must have a plug')
self.assertFormError(response, 'form', 'socket', 'A cable must have a socket')
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
def test_asset_edit(self):
url = reverse('asset_update', kwargs={'pk': self.asset.asset_id})
response = self.client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
# self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
self.assertFormError(response, 'form', 'description', 'This field is required.')
self.assertFormError(response, 'form', 'status', 'This field is required.')
self.assertFormError(response, 'form', 'category', 'This field is required.')
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
def test_cable_edit(self):
url = reverse('asset_update', kwargs={'pk': self.cable_asset.asset_id})
# TODO Why do I have to send is_cable=True here?
response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3, 'circuits': -4, 'cores': -8})
# Can't figure out how to select the 'none' option...
# self.assertFormError(response, 'form', 'plug', 'A cable must have a plug')
# self.assertFormError(response, 'form', 'socket', 'A cable must have a socket')
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
def test_asset_duplicate(self):
url = reverse('asset_duplicate', kwargs={'pk': self.cable_asset.asset_id})
response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0, 'circuits': 0, 'cores': 0})
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_generate_sample_data(self):
# Run the management command and check there are no exceptions
call_command('generateSampleAssetsData')
# Check there are lots
self.assertTrue(models.Asset.objects.all().count() > 50)
self.assertTrue(models.Supplier.objects.all().count() > 50)
@override_settings(DEBUG=True)
def test_delete_sample_data(self):
call_command('deleteSampleData')
self.assertTrue(models.Asset.objects.all().count() == 0)
self.assertTrue(models.Supplier.objects.all().count() == 0)
def test_production_exception(self):
from django.core.management.base import CommandError
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleAssetsData')
self.assertRaisesRegex(CommandError, ".*production", call_command, 'deleteSampleData')
class TestVersioningViews(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = rigsmodels.Profile.objects.create(username="VersionTest", email="version@test.com", is_superuser=True, is_active=True, is_staff=True)
working = models.AssetStatus.objects.create(name="Working", should_show=True)
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
general = models.AssetCategory.objects.create(name="General")
lighting = models.AssetCategory.objects.create(name="Lighting")
cls.assets = {}
with reversion.create_revision():
reversion.set_user(cls.profile)
cls.assets[1] = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=broken, category=lighting, date_acquired=datetime.date(1991, 12, 26))
with reversion.create_revision():
reversion.set_user(cls.profile)
cls.assets[2] = models.Asset.objects.create(asset_id="0001", description="Virgil", status=working, category=lighting, date_acquired=datetime.date(2015, 1, 1))
with reversion.create_revision():
reversion.set_user(cls.profile)
cls.assets[1].status = working
cls.assets[1].save()
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test_history_loads_successfully(self):
request_url = reverse('asset_history', kwargs={'pk': self.assets[1].asset_id})
response = self.client.get(request_url, follow=True)
self.assertEqual(response.status_code, 200)
def test_activity_table_loads_successfully(self):
request_url = reverse('asset_activity_table')
response = self.client.get(request_url, follow=True)
self.assertEqual(response.status_code, 200)
class TestEmbeddedViews(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = rigsmodels.Profile.objects.create(username="EmbeddedViewsTest", email="embedded@test.com", is_superuser=True, is_active=True, is_staff=True)
working = models.AssetStatus.objects.create(name="Working", should_show=True)
lighting = models.AssetCategory.objects.create(name="Lighting")
cls.assets = {
1: models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
}
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
def testLoginRedirect(self):
request_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects
response = self.client.get(request_url, follow=True)
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
# Now login
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
# And check that it no longer redirects
response = self.client.get(request_url, follow=True)
self.assertEqual(len(response.redirect_chain), 0)
def testLoginCookieWarning(self):
login_url = reverse('login_embed')
response = self.client.post(login_url, follow=True)
self.assertContains(response, "Cookies do not seem to be enabled")
def testXFrameHeaders(self):
asset_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
login_url = reverse('login_embed')
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
response = self.client.get(asset_url, follow=True)
with self.assertRaises(KeyError):
response._headers["X-Frame-Options"]
response = self.client.get(login_url, follow=True)
with self.assertRaises(KeyError):
response._headers["X-Frame-Options"]
def testOEmbed(self):
asset_url = reverse('asset_detail', kwargs={'pk': self.assets[1].asset_id})
asset_embed_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
oembed_url = reverse('asset_oembed', kwargs={'pk': self.assets[1].asset_id})
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
# Test the meta tag is in place
response = self.client.get(asset_url, follow=True, HTTP_HOST='example.com')
self.assertContains(response, '<link rel="alternate" type="application/json+oembed"')
self.assertContains(response, oembed_url)
# Test that the JSON exists
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, asset_embed_url)
# Should also work for non-existant
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, alt_asset_embed_url)

View File

@@ -3,12 +3,14 @@ from django.urls import path
from assets import views, models
from RIGS import versioning
from PyRIGS.decorators import permission_required_with_403
from django.contrib.auth.decorators import login_required
from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import has_oembed, permission_required_with_403
urlpatterns = [
path('', views.AssetList.as_view(), name='asset_index'),
path('asset/list/', views.AssetList.as_view(), name='asset_list'),
path('asset/id/<str:pk>/', views.AssetDetail.as_view(), name='asset_detail'),
path('', login_required(views.AssetList.as_view()), name='asset_index'),
path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'),
path('asset/id/<str:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'),
path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')
@@ -21,6 +23,13 @@ urlpatterns = [
(views.ActivityTable.as_view()), name='asset_activity_table'),
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
path('asset/id/<str:pk>/embed/',
xframe_options_exempt(
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
name='asset_embed'),
path('asset/id/<str:pk>/oembed_json/',
views.AssetOembed.as_view(),
name='asset_oembed'),
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
@@ -28,7 +37,7 @@ urlpatterns = [
(views.SupplierCreate.as_view()), name='supplier_create'),
path('supplier/<int:pk>/edit', permission_required_with_403('assets.change_supplier')
(views.SupplierUpdate.as_view()), name='supplier_update'),
path('supplier/<str:pk>/history/', views.SupplierVersionHistory.as_view(),
path('supplier/<int:pk>/history/', views.SupplierVersionHistory.as_view(),
name='supplier_history', kwargs={'model': models.Supplier}),
path('supplier/search/', views.SupplierSearch.as_view(), name='supplier_search_json'),

View File

@@ -1,5 +1,6 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.http import HttpResponse, Http404
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
@@ -9,6 +10,8 @@ from django.shortcuts import get_object_or_404
from assets import models, forms
from RIGS import versioning
import simplejson
@method_decorator(csrf_exempt, name='dispatch')
class AssetList(LoginRequiredMixin, generic.ListView):
@@ -84,8 +87,7 @@ class AssetIDUrlMixin:
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name})
raise Http404("No assets found matching the query")
return obj
@@ -149,6 +151,28 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
return context
class AssetOembed(generic.View):
model = models.Asset
def get(self, request, pk=None):
embed_url = reverse('asset_embed', args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
class AssetEmbed(AssetDetail):
template_name = 'asset_embed.html'
class SupplierList(generic.ListView):
model = models.Supplier
template_name = 'supplier_list.html'
@@ -188,7 +212,6 @@ class SupplierSearch(SupplierList):
for supplier in context["object_list"]:
result.append({"id": supplier.pk, "name": supplier.name})
return JsonResponse(result, safe=False)

View File

@@ -18,7 +18,7 @@ gunicorn==19.8.1
icalendar==4.0.1
lxml==4.2.1
Markdown==2.6.11
Pillow==5.1.0
Pillow==6.2.0
psycopg2==2.7.4
Pygments==2.2.0
PyPDF2==1.26.0
@@ -38,3 +38,4 @@ z3c.rml==3.5.0
zope.event==4.3.0
zope.interface==4.5.0
zope.schema==4.5.0
pypom==2.2.0

View File

@@ -52,7 +52,7 @@
{% endblock %}
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<li class="dropdown" id="user">
{% if user.is_authenticated %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="glyphicon glyphicon-user"></span>

View File

@@ -5,8 +5,10 @@
<dl class="dl-horizontal">
{% with form|nice_errors as qq %}
{% for error_name,desc in qq.items %}
<span>
<dt>{{error_name}}</dt>
<dd>{{desc}}</dd>
</span>
{% endfor %}
{% endwith %}
</dl>

View File

@@ -15,7 +15,7 @@
<div class="text-right">
<a href="{% url 'registration_register' %}" class="btn">Register</a>
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
<input type="submit" value="Login" class="btn btn-primary"/>
<input type="submit" id="id_submit" value="Login" class="btn btn-primary"/>
<input type="hidden" name="next" value="{{ next }}"/>
</div>
</form>