Compare commits

...

35 Commits

Author SHA1 Message Date
5174a442bc Try this way of parallel coverage 2021-01-27 20:21:29 +00:00
5f1fd59dd2 Fix screenshot uploading on CI (again) 2021-01-27 19:47:30 +00:00
3be2a9f4b5 Oops, remove obsolete if check 2021-01-27 19:39:24 +00:00
883ef4ed8b Bah, codestyle 2021-01-27 19:36:42 +00:00
2195a60438 Use pytest as our test runner for better parallelism
Also rewrote some asset tests to be in the pytest style. May do some more. Some warnings cleaned up in the process.
2021-01-27 19:33:20 +00:00
12c4b63947 Revert "Does this help the coverage be less weird?"
This reverts commit 39ab9df836.
2021-01-26 23:53:59 +00:00
39ab9df836 Does this help the coverage be less weird? 2021-01-26 23:36:26 +00:00
a99e4b1d1c What about this?
Swear I spend my life jiggerypokerying the damn test suite...
2021-01-26 21:51:59 +00:00
18a091f8ea What about this? 2021-01-26 19:42:54 +00:00
d64d0f54d4 Should fix asset test on CI 2021-01-26 19:37:12 +00:00
8f54897b69 Upload failure screenshots as individual artifacts not a zip
Turns out I can't unzip things from my phone, which is a pain
2021-01-26 19:31:06 +00:00
97830596b5 Fix unauth test to not just immediately pass out 2021-01-26 19:15:29 +00:00
68f401097d Add title checking to the slightly insane assets test 2021-01-26 17:43:01 +00:00
8c5b2f426d Refactor asset audit tests with better selectors
Also fixed a silly title error with the modal
2021-01-26 15:08:51 +00:00
e3fc05772e Exclude tests from the coverage stats
Seems to be artifically deflating our stats
2021-01-26 13:42:58 +00:00
e38205b9e7 Oops, remove unused import 2021-01-25 22:45:02 +00:00
f2b9642772 Switch to gh-a artifact uploading instead of imgur 'hack'
For test failure screenshots. Happy now @mattysmith22? ;p
2021-01-25 22:27:21 +00:00
08939a0e1f Purge old .idea config 2021-01-25 22:18:15 +00:00
15b7f9c7c1 No Ruby compass bodge, no need for rubocop! 2021-01-25 22:17:37 +00:00
c67f7e1e54 Purge old vagrant config 2021-01-25 22:16:25 +00:00
4f268b3168 Cache python dependencies
Should majorly speedup parallelillelelised testing
2021-01-25 22:08:32 +00:00
0e7723828a Update python version in tests 2021-01-25 21:59:23 +00:00
c46a2c53f3 Run parallelised RIGS tests as one matrix job 2021-01-25 21:55:32 +00:00
b3617a74bf Define service in coveralls task 2021-01-25 21:44:05 +00:00
5806cbc72d Change python ver 2021-01-25 21:42:55 +00:00
ab2ff9f146 Fix whoops in requirements.txt 2021-01-25 21:39:44 +00:00
fd5575a818 You valid now? 2021-01-25 21:37:22 +00:00
402a1dd7f0 Tends to help if I push valid yaml 2021-01-25 21:29:32 +00:00
be7688aa75 Upgrade python deps 2021-01-25 18:14:49 +00:00
a472e414f7 Add basic calendar button test
Mainly to pickup on FullCalendar loading errors
2021-01-25 18:09:21 +00:00
618c02aa9c Attempt at parallelising tests where possible 2021-01-25 16:42:12 +00:00
3056b6ef71 Fix audit time check in asset audit test 2021-01-25 16:07:44 +00:00
548c960996 npm upgrade 2021-01-25 15:50:36 +00:00
2cc43dcdb7 Move some gulp deps to dev rather than prod 2021-01-25 15:39:51 +00:00
6db25fb56b Upgrade to heroku-20 stack 2021-01-25 15:39:04 +00:00
56 changed files with 6527 additions and 6403 deletions

View File

@@ -6,3 +6,4 @@ plugins =
omit =
*/migrations/*
*/tests/*

View File

@@ -9,22 +9,25 @@ on:
jobs:
build:
env:
IMGUR_UPLOAD_CLIENT_ID: ${{ secrets.IMGUR_UPLOAD_CLIENT_ID }}
IMGUR_UPLOAD_CLIENT_SECRET: ${{ secrets.IMGUR_UPLOAD_CLIENT_SECRET }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
strategy:
max-parallel: 1
matrix:
python-version: [3.8]
test-group: ["RIGS", "versioning", "users", "assets"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
python-version: 3.9
- name: Cache python deps
uses: actions/cache@v2
with:
path: ${{ env.pythonLocation }}
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
- name: Setup Chromedriver
if: \!${{ matrix.parallel }}
run: |
wget https://chromedriver.storage.googleapis.com/2.36/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
@@ -35,7 +38,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install pycodestyle coverage coveralls django_coverage_plugin
pip install -r requirements.txt
pip install --upgrade --upgrade-strategy eager -r requirements.txt
python manage.py collectstatic --noinput
- name: Basic Checks
run: |
@@ -43,6 +46,28 @@ jobs:
python manage.py check
python manage.py makemigrations --check --dry-run
- name: Run Tests
run: |
coverage run manage.py test --verbosity=2
coveralls --service=github
run: coverage run -p -m pytest --cov=${{ matrix.test-group }} --cov-append -n 8 ${{ matrix.test-group }}/tests/
- uses: actions/upload-artifact@v2
if: failure()
with:
name: failure-screenshots ${{ matrix.test-group }}
path: screenshots/
retention-days: 5
- name: Upload Coverage
run: coverage combine && coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.test-group }}
COVERALLS_PARALLEL: true
coveralls:
name: Indicate completion to coveralls.io
needs: build
runs-on: ubuntu-latest
container: python:3-slim
steps:
- name: Finished
run: |
pip3 install --upgrade coveralls
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.idea/.name generated
View File

@@ -1 +0,0 @@
PyRIGS

5
.idea/encodings.xml generated
View File

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

8
.idea/modules.xml generated
View File

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

View File

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

7
.idea/vcs.xml generated
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,3 @@
*.sqlite3
*.scss
*.md
*.rb
Vagrantfile
config/vagrant/*
config/vagrant.yml

View File

@@ -263,6 +263,3 @@ USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
IMGUR_UPLOAD_CLIENT_ID = env('IMGUR_UPLOAD_CLIENT_ID', default="")
IMGUR_UPLOAD_CLIENT_SECRET = env('IMGUR_UPLOAD_CLIENT_SECRET', default="")

View File

@@ -6,7 +6,6 @@ import os
import pytz
from datetime import date, time, datetime, timedelta
from django.conf import settings
import imgurpython
import PyRIGS.settings
import sys
import pathlib
@@ -21,9 +20,6 @@ def create_datetime(year, month, day, hour, min):
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
# No caching, please and thank you
options.add_argument("--aggressive-cache-discard")
options.add_argument("--disk-cache-size=0")
options.add_argument("--headless")
if settings.CI:
options.add_argument("--no-sandbox")
@@ -62,19 +58,7 @@ def screenshot_failure(func):
if not pathlib.Path("screenshots").is_dir():
os.mkdir("screenshots")
self.driver.save_screenshot(screenshot_file)
if settings.IMGUR_UPLOAD_CLIENT_ID != "":
config = {
'album': None,
'name': screenshot_name,
'title': screenshot_name,
'description': ""
}
client = imgurpython.ImgurClient(settings.IMGUR_UPLOAD_CLIENT_ID, settings.IMGUR_UPLOAD_CLIENT_SECRET)
image = client.upload_from_path(screenshot_file, config=config)
print("Error in test {} is at url {}".format(screenshot_name, image['link']), file=sys.stderr)
else:
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
raise e
return wrapper_func

View File

@@ -1,5 +1,5 @@
from django.urls import path
from django.conf.urls import include, url
from django.urls import path, re_path
from django.conf.urls import include
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.contrib.auth.decorators import login_required
@@ -39,6 +39,6 @@ if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
re_path(r'^__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
] + urlpatterns

View File

@@ -24,7 +24,12 @@ from django.views.decorators.cache import never_cache, cache_page
from django.utils.decorators import method_decorator
def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
# Displays the current rig count along with a few other bits and pieces
class Index(generic.TemplateView):
template_name = 'index.html'
@@ -151,7 +156,7 @@ class SecureAPIRequest(generic.View):
class ModalURLMixin:
def get_close_url(self, update, detail):
if self.request.is_ajax():
if is_ajax(self.request):
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
@@ -170,7 +175,7 @@ class GenericListView(generic.ListView):
def get_context_data(self, **kwargs):
context = super(GenericListView, self).get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s"
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -202,7 +207,7 @@ class GenericDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super(GenericDetailView, self).get_context_data(**kwargs)
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -213,7 +218,7 @@ class GenericUpdateView(generic.UpdateView):
def get_context_data(self, **kwargs):
context = super(GenericUpdateView, self).get_context_data(**kwargs)
context['page_title'] = "Edit {}".format(self.model.__name__)
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -224,7 +229,7 @@ class GenericCreateView(generic.CreateView):
def get_context_data(self, **kwargs):
context = super(GenericCreateView, self).get_context_data(**kwargs)
context['page_title'] = "Create {}".format(self.model.__name__)
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/*!
* Bootstrap alert.js v4.5.2 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Bootstrap alert.js v4.6.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./util.js")):"function"==typeof define&&define.amd?define(["jquery","./util.js"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Alert=t(e.jQuery,e.Util)}(this,(function(e,t){"use strict";function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e,t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t;var r=e.fn.alert,o=function(){function r(e){this._element=e}var o,l,i,a=r.prototype;return a.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},a.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},a._getRootElement=function(n){var r=t.getSelectorFromElement(n),o=!1;return r&&(o=document.querySelector(r)),o||(o=e(n).closest(".alert")[0]),o},a._triggerCloseEvent=function(t){var n=e.Event("close.bs.alert");return e(t).trigger(n),n},a._removeElement=function(n){var r=this;if(e(n).removeClass("show"),e(n).hasClass("fade")){var o=t.getTransitionDurationFromElement(n);e(n).one(t.TRANSITION_END,(function(e){return r._destroyElement(n,e)})).emulateTransitionEnd(o)}else this._destroyElement(n)},a._destroyElement=function(t){e(t).detach().trigger("closed.bs.alert").remove()},r._jQueryInterface=function(t){return this.each((function(){var n=e(this),o=n.data("bs.alert");o||(o=new r(this),n.data("bs.alert",o)),"close"===t&&o[t](this)}))},r._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},o=r,i=[{key:"VERSION",get:function(){return"4.5.2"}}],(l=null)&&n(o.prototype,l),i&&n(o,i),r}();return e(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',o._handleDismiss(new o)),e.fn.alert=o._jQueryInterface,e.fn.alert.Constructor=o,e.fn.alert.noConflict=function(){return e.fn.alert=r,o._jQueryInterface},o}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./util.js")):"function"==typeof define&&define.amd?define(["jquery","./util"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Alert=t(e.jQuery,e.Util)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var r=n(e),l=n(t);function u(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}var a="bs.alert",o=r.default.fn.alert,i=function(){function e(e){this._element=e}var t,n,o,i=e.prototype;return i.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},i.dispose=function(){r.default.removeData(this._element,a),this._element=null},i._getRootElement=function(e){var t=l.default.getSelectorFromElement(e),n=!1;return t&&(n=document.querySelector(t)),n||(n=r.default(e).closest(".alert")[0]),n},i._triggerCloseEvent=function(e){var t=r.default.Event("close.bs.alert");return r.default(e).trigger(t),t},i._removeElement=function(e){var t=this;if(r.default(e).removeClass("show"),r.default(e).hasClass("fade")){var n=l.default.getTransitionDurationFromElement(e);r.default(e).one(l.default.TRANSITION_END,(function(n){return t._destroyElement(e,n)})).emulateTransitionEnd(n)}else this._destroyElement(e)},i._destroyElement=function(e){r.default(e).detach().trigger("closed.bs.alert").remove()},e._jQueryInterface=function(t){return this.each((function(){var n=r.default(this),l=n.data(a);l||(l=new e(this),n.data(a,l)),"close"===t&&l[t](this)}))},e._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},t=e,o=[{key:"VERSION",get:function(){return"4.6.0"}}],(n=null)&&u(t.prototype,n),o&&u(t,o),e}();return r.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',i._handleDismiss(new i)),r.default.fn.alert=i._jQueryInterface,r.default.fn.alert.Constructor=i,r.default.fn.alert.noConflict=function(){return r.default.fn.alert=o,i._jQueryInterface},i}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/*!
* Bootstrap popover.js v4.5.2 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Bootstrap popover.js v4.6.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery"),require("./tooltip.js")):"function"==typeof define&&define.amd?define(["jquery","./tooltip.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Popover=e(t.jQuery,t.Tooltip)}(this,(function(t,e){"use strict";function n(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function o(){return(o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(t[o]=n[o])}return t}).apply(this,arguments)}t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t,e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e;var r="popover",i=".bs.popover",s=t.fn[r],p=new RegExp("(^|\\s)bs-popover\\S+","g"),u=o({},e.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),a=o({},e.DefaultType,{content:"(string|element|function)"}),l={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},c=function(e){var o,s;function c(){return e.apply(this,arguments)||this}s=e,(o=c).prototype=Object.create(s.prototype),o.prototype.constructor=o,o.__proto__=s;var f,h,d,y=c.prototype;return y.isWithContent=function(){return this.getTitle()||this._getContent()},y.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},y.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},y.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(".popover-header"),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(".popover-body"),n),e.removeClass("fade show")},y._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},y._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(p);null!==n&&n.length>0&&e.removeClass(n.join(""))},c._jQueryInterface=function(e){return this.each((function(){var n=t(this).data("bs.popover"),o="object"==typeof e?e:null;if((n||!/dispose|hide/.test(e))&&(n||(n=new c(this,o),t(this).data("bs.popover",n)),"string"==typeof e)){if(void 0===n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},f=c,d=[{key:"VERSION",get:function(){return"4.5.2"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return r}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return l}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return a}}],(h=null)&&n(f.prototype,h),d&&n(f,d),c}(e);return t.fn[r]=c._jQueryInterface,t.fn[r].Constructor=c,t.fn[r].noConflict=function(){return t.fn[r]=s,c._jQueryInterface},c}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery"),require("./tooltip.js")):"function"==typeof define&&define.amd?define(["jquery","./tooltip"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Popover=e(t.jQuery,t.Tooltip)}(this,(function(t,e){"use strict";function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=n(t),r=n(e);function i(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function u(){return(u=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(t[o]=n[o])}return t}).apply(this,arguments)}var f="popover",a="bs.popover",l="."+a,s=o.default.fn[f],c=new RegExp("(^|\\s)bs-popover\\S+","g"),p=u({},r.default.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),d=u({},r.default.DefaultType,{content:"(string|element|function)"}),h={HIDE:"hide"+l,HIDDEN:"hidden"+l,SHOW:"show"+l,SHOWN:"shown"+l,INSERTED:"inserted"+l,CLICK:"click"+l,FOCUSIN:"focusin"+l,FOCUSOUT:"focusout"+l,MOUSEENTER:"mouseenter"+l,MOUSELEAVE:"mouseleave"+l},y=function(t){var e,n;function r(){return t.apply(this,arguments)||this}n=t,(e=r).prototype=Object.create(n.prototype),e.prototype.constructor=e,e.__proto__=n;var u,s,y,v=r.prototype;return v.isWithContent=function(){return this.getTitle()||this._getContent()},v.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-popover-"+t)},v.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},v.setContent=function(){var t=o.default(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(".popover-body"),e),t.removeClass("fade show")},v._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},v._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(c);null!==e&&e.length>0&&t.removeClass(e.join(""))},r._jQueryInterface=function(t){return this.each((function(){var e=o.default(this).data(a),n="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new r(this,n),o.default(this).data(a,e)),"string"==typeof t)){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},u=r,y=[{key:"VERSION",get:function(){return"4.6.0"}},{key:"Default",get:function(){return p}},{key:"NAME",get:function(){return f}},{key:"DATA_KEY",get:function(){return a}},{key:"Event",get:function(){return h}},{key:"EVENT_KEY",get:function(){return l}},{key:"DefaultType",get:function(){return d}}],(s=null)&&i(u.prototype,s),y&&i(u,y),r}(r.default);return o.default.fn[f]=y._jQueryInterface,o.default.fn[f].Constructor=y,o.default.fn[f].noConflict=function(){return o.default.fn[f]=s,y._jQueryInterface},y}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/*!
* Bootstrap util.js v4.5.2 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Bootstrap util.js v4.6.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Util=e(t.jQuery)}(this,(function(t){"use strict";t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t;function e(e){var r=this,o=!1;return t(this).one(n.TRANSITION_END,(function(){o=!0})),setTimeout((function(){o||n.triggerTransitionEnd(r)}),e),this}var n={TRANSITION_END:"bsTransitionEnd",getUID:function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(e){if(!e)return 0;var n=t(e).css("transition-duration"),r=t(e).css("transition-delay"),o=parseFloat(n),i=parseFloat(r);return o||i?(n=n.split(",")[0],r=r.split(",")[0],1e3*(parseFloat(n)+parseFloat(r))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(e){t(e).trigger("transitionend")},supportsTransitionEnd:function(){return Boolean("transitionend")},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,r){for(var o in r)if(Object.prototype.hasOwnProperty.call(r,o)){var i=r[o],a=e[o],u=a&&n.isElement(a)?"element":null==(s=a)?""+s:{}.toString.call(s).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(i).test(u))throw new Error(t.toUpperCase()+': Option "'+o+'" provided type "'+u+'" but expected type "'+i+'".')}var s},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){var e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?n.findShadowRoot(t.parentNode):null},jQueryDetection:function(){if(void 0===t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};return n.jQueryDetection(),t.fn.emulateTransitionEnd=e,t.event.special[n.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}},n}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Util=e(t.jQuery)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t),r="transitionend";function o(t){var e=this,r=!1;return n.default(this).one(i.TRANSITION_END,(function(){r=!0})),setTimeout((function(){r||i.triggerTransitionEnd(e)}),t),this}var i={TRANSITION_END:"bsTransitionEnd",getUID:function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(t){if(!t)return 0;var e=n.default(t).css("transition-duration"),r=n.default(t).css("transition-delay"),o=parseFloat(e),i=parseFloat(r);return o||i?(e=e.split(",")[0],r=r.split(",")[0],1e3*(parseFloat(e)+parseFloat(r))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(t){n.default(t).trigger(r)},supportsTransitionEnd:function(){return Boolean(r)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var r in n)if(Object.prototype.hasOwnProperty.call(n,r)){var o=n[r],a=e[r],u=a&&i.isElement(a)?"element":null==(l=a)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(o).test(u))throw new Error(t.toUpperCase()+': Option "'+r+'" provided type "'+u+'" but expected type "'+o+'".')}var l},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){var e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?i.findShadowRoot(t.parentNode):null},jQueryDetection:function(){if(void 0===n.default)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=n.default.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||t[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};return i.jQueryDetection(),n.default.fn.emulateTransitionEnd=o,n.default.event.special[i.TRANSITION_END]={bindType:r,delegateType:r,handle:function(t){if(n.default(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}},i}));

View File

@@ -304,3 +304,43 @@ class UserPage(BasePage):
def generate_key(self):
self.find_element(*self._generation_button_selector).click()
class CalendarPage(BasePage):
URL_TEMPLATE = 'rigboard/calendar'
_go_locator = (By.ID, 'go-to-date-button')
_today_selector = (By.ID, 'today-button')
_prev_selector = (By.ID, 'prev-button')
_next_selector = (By.ID, 'next-button')
_month_selector = (By.ID, 'month-button')
_week_selector = (By.ID, 'week-button')
_day_selector = (By.ID, 'day-button')
def go(self):
return self.find_element(*self._go_locator).click()
@property
def target_date(self):
return regions.DatePicker(self, self.find_element(By.ID, 'go-to-date-input'))
def today(self):
return self.find_element(*self._today_selector).click()
def prev(self):
return self.find_element(*self._prev_selector).click()
def next(self):
return self.find_element(*self._next_selector).click()
def month(self):
return self.find_element(*self._month_selector).click()
def week(self):
return self.find_element(*self._week_selector).click()
def day(self):
return self.find_element(*self._day_selector).click()

View File

@@ -645,7 +645,18 @@ class TestCalendar(BaseRigboardTest):
else:
self.assertNotContains(response, "TE E" + str(test) + " ")
# Wow - that was a lot of tests
def test_calendar_buttons(self): # If FullCalendar fails to load for whatever reason, the buttons don't work
self.page = pages.CalendarPage(self.driver, self.live_server_url).open()
self.assertIn(timezone.now().strftime("%Y-%m"), self.driver.current_url)
target_date = datetime.date(2020, 1, 1)
self.page.target_date.set_value(target_date)
self.page.go()
self.assertIn(self.page.target_date.value.strftime("%Y-%m"), self.driver.current_url)
self.page.next()
target_date += datetime.timedelta(days=32)
self.assertIn(target_date.strftime("%m"), self.driver.current_url)
@screenshot_failure_cls

View File

@@ -1,6 +1,6 @@
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.urls import path, re_path
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import RedirectView
from PyRIGS.decorators import (api_key_required, has_oembed,
@@ -45,10 +45,10 @@ urlpatterns = [
path('rigboard/', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(rigboard.WebCalendar.as_view()),
name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
@@ -130,12 +130,12 @@ urlpatterns = [
path('event/<int:pk>/auth/preview/',
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthoriseRequestEmailPreview.as_view()),
name='event_authorise_preview'),
url(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
name='event_authorise'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
name='event_authorise'),
# ICS Calendar - API key authentication
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
name="ics_calendar"),
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
name="ics_calendar"),
# Legacy URLs

134
Vagrantfile vendored
View File

@@ -1,134 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
require 'yaml'
unless File.exist?('config/vagrant.yml')
raise "There is no config/vagrant.yml file.\nCopy config/vagrant.template.yml, make any changes you need, then try again."
end
settings = YAML.load_file 'config/vagrant.yml'
$script = <<SCRIPT
echo Beginning Vagrant provisioning...
date > /etc/vagrant_provisioned_at
SCRIPT
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = '2'
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.provision 'shell', inline: $script
# Every Vagrant development environment requires a box. You can search for
# boxes at https://atlas.hashicorp.com/search.
config.vm.box = 'ubuntu/trusty64'
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
config.vm.synced_folder ".", "/vagrant"
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:5000" will access port 5000 on the guest machine.
config.vm.network "forwarded_port", guest: 5000, host: 5000
# PostgreSQL Server port forwarding
config.vm.network "forwarded_port", host: 15432, guest: 5432
# You can provision with just one of these scripts by user its name, eg:
# $ vagrant provision --provision-with postgresql
config.vm.provision 'build',
type: 'shell',
path: 'config/vagrant/build_dependency_setup.sh'
config.vm.provision 'git',
type: 'shell',
path: 'config/vagrant/git_setup.sh'
config.vm.provision 'postgresql',
type: 'shell',
path: 'config/vagrant/postgresql_setup.sh',
args: [
settings['db']['name'],
settings['db']['user'],
settings['db']['password'],
]
config.vm.provision 'python',
type: 'shell',
path: 'config/vagrant/python_setup.sh'
config.vm.provision 'virtualenv',
type: 'shell',
path: 'config/vagrant/virtualenv_setup.sh',
args: [
settings['virtualenv']['envname'],
]
# Will install foreman and, if there's a Procfile, start it:
config.vm.provision 'foreman',
type: 'shell',
path: 'config/vagrant/foreman_setup.sh',
run: 'always',
args: [
settings['virtualenv']['envname'],
settings['django']['settings_module'],
settings['foreman']['procfile'],
]
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
vb.memory = "1024"
end
#
# View the documentation for the provider you are using for more
# information on available options.
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
# such as FTP and Heroku are also available. See the documentation at
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
# config.push.define "atlas" do |push|
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
# end
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# sudo apt-get update
# sudo apt-get install -y apache2
# SHELL
end

View File

@@ -4,7 +4,7 @@
"scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
},
"stack": "heroku-18",
"stack": "heroku-20",
"env": {
"DEBUG": {
"required": true
@@ -51,4 +51,4 @@
"url": "heroku/python"
}
]
}
}

View File

@@ -1,6 +1,5 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Audit Asset {{ object.asset_id }}{% endblock %}
{% block content %}
<script>

View File

@@ -209,6 +209,8 @@ class AssetAuditList(AssetList):
_errors_selector = (By.CLASS_NAME, "alert-danger")
# Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button...
_submit_locator = (By.ID, "id_mark_audited")
_close_selector = (By.XPATH, "//button[@data-dismiss='modal']")
form_items = {
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
'description': (regions.TextBox, (By.ID, 'id_description')),
@@ -240,6 +242,10 @@ class AssetAuditList(AssetList):
self.root.find_element(*self._submit_locator).click()
# self.wait.until(lambda x: not self.is_displayed) TODO
def close(self):
previous_errors = self.errors
self.page.find_element(*self._close_selector).click()
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\")});")

View File

@@ -294,31 +294,26 @@ class TestAssetAudit(AutoLoginTest):
asset_id = "1111"
self.page.set_query(asset_id)
self.page.search()
mdl = self.page.modal
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
# Do it wrong on purpose to check error display
mdl.remove_all_required()
mdl.description = ""
mdl.submit()
# self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
self.page.modal.remove_all_required()
self.page.modal.description = ""
self.page.modal.submit()
self.wait.until(animation_is_finished())
# self.assertTrue(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("This field is required.", mdl.errors["Description"])
self.assertIn("This field is required.", self.page.modal.errors["Description"])
# Now do it properly
new_desc = "A BIG hammer"
mdl.description = new_desc
mdl.submit()
self.page.modal.description = new_desc = "A BIG hammer"
self.page.modal.submit()
submit_time = timezone.now()
self.wait.until(EC.invisibility_of_element_located((By.ID, 'modal')))
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Check data is correct
audited = models.Asset.objects.get(asset_id="1111")
audited = models.Asset.objects.get(asset_id=asset_id)
self.assertEqual(audited.description, new_desc)
# Make sure audit 'log' was filled out
self.assertEqual(self.profile.initials, audited.last_audited_by.initials)
self.assertEqual(timezone.now().date(), audited.last_audited_at.date())
self.assertEqual(timezone.now().hour, audited.last_audited_at.hour)
self.assertEqual(timezone.now().minute, audited.last_audited_at.minute)
self.assertEqual(submit_time.replace(microsecond=0), audited.last_audited_at.replace(microsecond=0))
# Check we've removed it from the 'needing audit' list
self.assertNotIn(asset_id, self.page.assets)
@@ -326,297 +321,20 @@ class TestAssetAudit(AutoLoginTest):
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
asset_row = self.page.assets[0]
self.driver.find_element(By.XPATH, "//*[@id='asset_table_body']/tr[1]/td[4]/a").click()
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
self.assertEqual(self.page.modal.asset_id, asset_row.id)
# First close button is for the not found error
self.page.find_element(By.XPATH, '//*[@id="modal"]/div/div/div[1]/button').click()
self.page.modal.close()
self.wait.until(EC.invisibility_of_element_located((By.ID, 'modal')))
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=asset_row.id)
self.assertEqual(None, audited.last_audited_by)
def test_audit_search(self):
# Check that a failed search works
self.page.set_query("NOTFOUND")
self.page.search()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("Asset with that ID does not exist!", self.page.error.text)
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)
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_type = models.CableType.objects.create(circuits=11, cores=3, plug=cls.connector, socket=cls.connector)
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, cable_type=cls.cable_type, length=10, csa="1.5")
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', 'cable_type', 'A cable must have a type')
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')
# 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})
# TODO Can't figure out how to select the 'none' option...
# self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
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')
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})
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')
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 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)

296
assets/tests/test_unit.py Normal file
View File

@@ -0,0 +1,296 @@
from django.core.management import call_command
from assets import models
from django.test.utils import override_settings
from django.urls import reverse
from urllib.parse import urlparse
from assets import models, urls
from reversion import revisions as reversion
import datetime
from django.utils import timezone
from django.test import tag
import pytest
from pytest_django.asserts import assertFormError, assertRedirects
pytestmark = pytest.mark.django_db # TODO
def response_contains(response, needle):
return needle in str(response.content)
def login(client, django_user_model):
pwd = 'testuser'
usr = "TestUser"
profile = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd, is_superuser=True, is_active=True, is_staff=True)
assert client.login(username=usr, password=pwd)
def create_test_asset():
working = models.AssetStatus.objects.create(name="Working", should_show=True)
lighting = models.AssetCategory.objects.create(name="Lighting")
asset = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
return asset
def create_test_cable():
category = models.AssetCategory.objects.create(name="Sound")
status = models.AssetStatus.objects.create(name="Broken", should_show=True)
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
return models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5")
def test_supplier_create(client, django_user_model):
login(client, django_user_model)
url = reverse('supplier_create')
response = client.post(url)
assertFormError(response, 'form', 'name', 'This field is required.')
def test_supplier_edit(client, django_user_model):
login(client, django_user_model)
supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
url = reverse('supplier_update', kwargs={'pk': supplier.pk})
response = client.post(url, {'name': ""})
assertFormError(response, 'form', 'name', 'This field is required.')
def test_404(client, django_user_model):
login(client, django_user_model)
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 = client.get(request_url, follow=True)
assert response.status_code == 404
def test_embed_login_redirect(client, django_user_model):
request_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects
response = client.get(request_url, follow=True)
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
# Now login
login(client, django_user_model)
# And check that it no longer redirects
response = client.get(request_url, follow=True)
assert len(response.redirect_chain) == 0
def test_login_cookie_warning(client, django_user_model):
login_url = reverse('login_embed')
response = client.post(login_url, follow=True)
assert "Cookies do not seem to be enabled" in str(response.content)
def test_x_frame_headers(client, django_user_model):
asset_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id})
login_url = reverse('login_embed')
login(client, django_user_model)
response = client.get(asset_url, follow=True)
with pytest.raises(KeyError):
response._headers["X-Frame-Options"]
response = client.get(login_url, follow=True)
with pytest.raises(KeyError):
response._headers["X-Frame-Options"]
def test_oembed(client):
asset = create_test_asset()
asset_url = reverse('asset_detail', kwargs={'pk': asset.asset_id})
asset_embed_url = reverse('asset_embed', kwargs={'pk': asset.asset_id})
oembed_url = reverse('asset_oembed', kwargs={'pk': asset.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 = client.get(asset_url, follow=True, HTTP_HOST='example.com')
assert '<link rel="alternate" type="application/json+oembed"' in str(response.content)
assert oembed_url in str(response.content)
# Test that the JSON exists
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assert asset_embed_url in str(response.content)
# Should also work for non-existant
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assert alt_asset_embed_url in str(response.content)
@override_settings(DEBUG=True)
def test_generate_sample_data(client):
# Run the management command and check there are no exceptions
call_command('generateSampleAssetsData')
# Check there are lots
assert models.Asset.objects.all().count() > 50
assert models.Supplier.objects.all().count() > 50
@override_settings(DEBUG=True)
def test_delete_sample_data(client):
call_command('deleteSampleData')
assert models.Asset.objects.all().count() == 0
assert models.Supplier.objects.all().count() == 0
def test_production_exception(client):
from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"):
call_command('generateSampleAssetsData')
call_command('deleteSampleData')
def test_asset_create(client, django_user_model):
login(client, django_user_model)
response = client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
assertFormError(response, 'form', 'asset_id', 'This field is required.')
assertFormError(response, 'form', 'description', 'This field is required.')
assertFormError(response, 'form', 'status', 'This field is required.')
assertFormError(response, 'form', 'category', 'This field is required.')
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
def test_cable_create(client, django_user_model):
login(client, django_user_model)
response = client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True})
assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
def test_asset_edit(client, django_user_model):
login(client, django_user_model)
url = reverse('asset_update', kwargs={'pk': create_test_asset().asset_id})
response = client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
# assertFormError(response, 'form', 'asset_id', 'This field is required.')
assertFormError(response, 'form', 'description', 'This field is required.')
assertFormError(response, 'form', 'status', 'This field is required.')
assertFormError(response, 'form', 'category', 'This field is required.')
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
def test_cable_edit(client, django_user_model):
login(client, django_user_model)
url = reverse('asset_update', kwargs={'pk': create_test_cable().asset_id})
# TODO Why do I have to send is_cable=True here?
response = client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
# TODO Can't figure out how to select the 'none' option...
# assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
def test_asset_duplicate(client, django_user_model):
login(client, django_user_model)
url = reverse('asset_duplicate', kwargs={'pk': create_test_cable().asset_id})
response = client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
@override_settings(DEBUG=True)
def create_asset_one():
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production
call_command('generateSampleData')
# Create an asset with ID 1 to make things easier in loops (we can always use pk=1)
category = models.AssetCategory.objects.create(name="Number One")
status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
return models.Asset.objects.create(asset_id="1", description="Half Price Fish", status=status, category=category, date_acquired=datetime.date(2020, 2, 1))
# Nothing should be available to the unauthenticated
def test_unauthenticated(client):
for url in filter(lambda url: url.name is not None and "json" not in str(url), urls.urlpatterns):
pattern = str(url.pattern)
request_url = ""
if ":pk>" in pattern:
request_url = reverse(url.name, kwargs={'pk': 1})
else:
request_url = reverse(url.name)
if request_url:
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
# TODO Check the URL here
assert response_contains(response, 'Login')
def test_basic_access(client):
create_asset_one()
client.login(username="basic", password="basic")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons NOT shown in list
assert not response_contains(response, 'Edit')
assert not response_contains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': "9000"})
response = client.get(url)
assert not response_contains(response, 'Purchase Details')
assert not response_contains(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 = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_update', kwargs={'pk': "1"})
response = client.get(request_url, follow=True)
assert response.status_code == 403
def test_keyholder_access(client):
create_asset_one()
client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons shown in list
assert response_contains(response, 'Edit')
assert response_contains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': "9000"})
response = client.get(url)
assert response_contains(response, 'Purchase Details')
assert response_contains(response, 'View Revision History')
def test_page_titles(admin_client):
for url in filter(lambda url: url.name is not None and not any(s in url.name for s in ["json", "embed"]), urls.urlpatterns):
request_url = ""
if ":pk>" in str(url.pattern):
request_url = reverse(url.name, kwargs={'pk': "1"})
else:
request_url = reverse(url.name)
response = admin_client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = response.context_data["page_title"]
assert response_contains(response, '<title>{} | Rig Information Gathering System</title>'.format(expected_title))

View File

@@ -14,7 +14,7 @@ from django.utils.decorators import method_decorator
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from versioning import versioning
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, is_ajax
from itertools import chain
@@ -121,7 +121,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
return context
def get_success_url(self):
if self.request.is_ajax():
if is_ajax(self.request):
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
@@ -211,6 +211,11 @@ class AssetAudit(AssetEdit):
template_name = 'asset_audit.html'
form_class = forms.AssetAuditForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Audit Asset: {}".format(self.object.display_id)
return context
def get_success_url(self):
# TODO For some reason this doesn't stick when done in form_valid??
asset = self.get_object()
@@ -230,7 +235,7 @@ class SupplierList(GenericListView):
context['edit'] = 'supplier_update'
context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
context['detail'] = 'supplier_detail'
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -258,7 +263,7 @@ class SupplierDetail(GenericDetailView):
context['detail_link'] = 'supplier_detail'
context['associated'] = 'partials/associated_assets.html'
context['associated2'] = ''
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -272,7 +277,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin):
def get_context_data(self, **kwargs):
context = super(SupplierCreate, self).get_context_data(**kwargs)
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -288,7 +293,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin):
def get_context_data(self, **kwargs):
context = super(SupplierUpdate, self).get_context_data(**kwargs)
if self.request.is_ajax():
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'

View File

@@ -1,11 +0,0 @@
django:
settings_module: PyRIGS.settings
virtualenv:
envname: myvirtualenv
foreman:
procfile: Procfile
db:
name: postgres_db
user: postgres_db
password: postgres_db

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
# Via https://github.com/kiere/vagrant-heroku-cedar-14/blob/master/config/vagrant/build_dependency_setup.sh
echo "=== Begin Vagrant Provisioning using 'config/vagrant/build_dependency_setup.sh'"
# Install build dependencies for a sane build environment
apt-get -y update
apt-get -y install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev
# Other things that we may need installed before anything else.
apt-get install -y libmemcached-dev
apt-get build-dep python-lxml
echo "=== End Vagrant Provisioning using 'config/vagrant/build_dependency_setup.sh'"

View File

@@ -1,36 +0,0 @@
#!/bin/bash
# Install and (if there's a Procfile) start foreman.
# Needs to come after the virtualenv has been set up.
# Expects three arguments:
VIRTUALENV_NAME=$1
DJANGO_SETTINGS_MODULE=$2
PROCFILE=$3
echo "=== Begin Vagrant Provisioning using 'config/vagrant/foreman_setup.sh'"
gem install foreman --no-ri --no-rdoc
if ! grep -Fq "DJANGO_SETTINGS_MODULE" /home/vagrant/.bashrc; then
echo "export DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE}" >> /home/vagrant/.bashrc
fi
if [[ -f /vagrant/$PROCFILE ]]; then
echo "Procfile found; starting foreman."
export DJANGO_SETTINGS_MODULE="$DJANGO_SETTINGS_MODULE"
# Ensure the virtualenv settings in .profile are loaded:
source /home/vagrant/.profile
# Run with & to release the terminal.
# Although that may also rely on the Procfile's processes having their
# output sent to a file, not stdout/stderr.
foreman start -f /vagrant/$PROCFILE &
else
echo "No Procfile found; not starting foreman."
fi
echo "=== End Vagrant Provisioning using 'config/vagrant/foreman_setup.sh'"

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
# Via https://github.com/kiere/vagrant-heroku-cedar-14/blob/master/config/vagrant/git_setup.sh
echo "=== Begin Vagrant Provisioning using 'config/vagrant/git_setup.sh'"
# Install Git if not available
if [ -z `which git` ]; then
echo "===== Installing Git"
apt-get -y update
apt-get -y install git-core
fi
echo "=== End Vagrant Provisioning using 'config/vagrant/git_setup.sh'"

View File

@@ -1,112 +0,0 @@
#!/bin/sh -e
# Expects three arguments:
# $1 - database name
# $2 - database user
# #3 - database user password
# Via https://github.com/kiere/vagrant-heroku-cedar-14/blob/master/config/vagrant/postgresql_setup.sh
echo "=== Begin Vagrant Provisioning using 'config/vagrant/postgresql_setup.sh'"
APP_DB_NAME=$1
APP_DB_USER=$2
APP_DB_PASS=$3
# Edit the following to change the version of PostgreSQL that is installed
PG_VERSION=9.4
###########################################################
# Changes below this line are probably not necessary
###########################################################
print_db_usage () {
echo "Your PostgreSQL database has been setup and can be accessed on your local machine on the forwarded port (default: 15432)"
echo " Host: localhost"
echo " Port: 15432"
echo " Database: $APP_DB_NAME"
echo " Username: $APP_DB_USER"
echo " Password: $APP_DB_PASS"
echo ""
echo "Admin access to postgres user via VM:"
echo " vagrant ssh"
echo " sudo su - postgres"
echo ""
echo "psql access to app database user via VM:"
echo " vagrant ssh"
echo " sudo su - postgres"
echo " PGUSER=$APP_DB_USER PGPASSWORD=$APP_DB_PASS psql -h localhost $APP_DB_NAME"
echo ""
echo "Env variable for application development:"
echo " DATABASE_URL=postgresql://$APP_DB_USER:$APP_DB_PASS@localhost:15432/$APP_DB_NAME"
echo ""
echo "Local command to access the database via psql:"
echo " PGUSER=$APP_DB_USER PGPASSWORD=$APP_DB_PASS psql -h localhost -p 15432 $APP_DB_NAME"
}
export DEBIAN_FRONTEND=noninteractive
PROVISIONED_ON=/etc/vm_provision_on_timestamp
if [ -f "$PROVISIONED_ON" ]
then
echo "VM was already provisioned at: $(cat $PROVISIONED_ON)"
echo "To run system updates manually login via 'vagrant ssh' and run 'apt-get update && apt-get upgrade'"
echo ""
print_db_usage
exit
fi
PG_REPO_APT_SOURCE=/etc/apt/sources.list.d/pgdg.list
if [ ! -f "$PG_REPO_APT_SOURCE" ]
then
# Add PG apt repo:
echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > "$PG_REPO_APT_SOURCE"
# Add PGDG repo key:
wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | apt-key add -
fi
# Update package list and upgrade all packages
apt-get update
apt-get -y upgrade
apt-get -y install "postgresql-$PG_VERSION" "postgresql-contrib-$PG_VERSION"
apt-get -y install libpq-dev # For building ruby 'pg' gem
PG_CONF="/etc/postgresql/$PG_VERSION/main/postgresql.conf"
PG_HBA="/etc/postgresql/$PG_VERSION/main/pg_hba.conf"
PG_DIR="/var/lib/postgresql/$PG_VERSION/main"
# Edit postgresql.conf to change listen address to '*':
sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" "$PG_CONF"
# Append to pg_hba.conf to add password auth:
echo "host all all all md5" >> "$PG_HBA"
# Explicitly set default client_encoding
echo "client_encoding = utf8" >> "$PG_CONF"
# Restart so that all new config is loaded:
service postgresql restart
cat << EOF | su - postgres -c psql
-- Create the database user:
CREATE USER $APP_DB_USER PASSWORD '$APP_DB_PASS';
EOF
cat << EOF | su - postgres -c psql
-- Create the database:
CREATE DATABASE $APP_DB_NAME WITH OWNER=$APP_DB_USER
LC_COLLATE='en_US.utf8'
LC_CTYPE='en_US.utf8'
ENCODING='UTF8'
TEMPLATE=template0;
EOF
# Tag the provision time:
date > "$PROVISIONED_ON"
echo "Successfully created PostgreSQL dev virtual machine."
echo ""
print_db_usage
echo "=== End Vagrant Provisioning using 'config/vagrant/postgresql_setup.sh'"

View File

@@ -1,23 +0,0 @@
#!/bin/bash
# Install python and required python modules.
# pip and virtualenv are in virtualenv_setup.sh
# Initial part of this via
# https://github.com/torchbox/vagrant-django-base/blob/master/install.sh
echo "=== Begin Vagrant Provisioning using 'config/vagrant/python_setup.sh'"
apt-get update -y
# Python dev packages
apt-get install -y python python-dev python-setuptools python-pip
# Dependencies for image processing with Pillow (drop-in replacement for PIL)
# supporting: jpeg, tiff, png, freetype, littlecms
apt-get install -y libjpeg-dev libtiff-dev zlib1g-dev libfreetype6-dev liblcms2-dev
# lxml dependencies
apt-get install -y libxml2-dev libxslt1-dev python-dev
echo "=== End Vagrant Provisioning using 'config/vagrant/python_setup.sh'"

View File

@@ -1,66 +0,0 @@
#!/bin/bash
# This will:
# * Install pip, virtualenv and virtualenvwrapper.
# * Set up virtualenvwrapper's paths etc.
# * Create a new virtualenv.
# * Install any pip requirements from the requirements.txt file.
# Expects one argument, the name of the virtualenv.
# The name we'll use for the virtualenv in which we'll install requirements:
VENV_NAME=$1
echo "=== Begin Vagrant Provisioning using 'config/vagrant/virtualenv_setup.sh'"
# virtualenv global setup
if ! command -v pip; then
easy_install -U pip
fi
if [[ ! -f /usr/local/bin/virtualenv ]]; then
easy_install virtualenv virtualenvwrapper
fi
# If it doesn't look like .bashrc has the required virtualenvwrapper lines in,
# then add them.
if ! grep -Fq "WORKON_HOME" /home/vagrant/.profile; then
echo "Adding virtualenvwrapper locations to .profile"
if [[ -d /vagrant/config/virtualenvwrapper/vagrant ]]; then
echo "export VIRTUALENVWRAPPER_HOOK_DIR=/vagrant/config/virtualenvwrapper/vagrant" >> /home/vagrant/.profile
fi
echo "export WORKON_HOME=/home/vagrant/.virtualenvs" >> /home/vagrant/.profile
echo "export PROJECT_HOME=/home/vagrant/Devel" >> /home/vagrant/.profile
echo "source /usr/local/bin/virtualenvwrapper.sh" >> /home/vagrant/.profile
fi
# Get .virtualenvwrapper env variables set up:
source /home/vagrant/.profile
if [[ -d /home/vagrant/.virtualenvs/$VENV_NAME ]]; then
echo "Activating virtualenv $VENV_NAME."
workon $VENV_NAME
else
echo "Making new virtualenv $VENV_NAME."
# Also switches to the virtualenv:
mkvirtualenv $VENV_NAME
# So that we can install things with pip while ssh'd in as vagrant user:
sudo chown -R vagrant:vagrant /home/vagrant/.virtualenvs/$VENV_NAME/
# Automatically switch to the virtual env on log in:
echo "workon $VENV_NAME" >> /home/vagrant/.profile
fi
# If we have a requirements.txt file in this project, then install
# everything in it with pip in a new virtualenv.
if [[ -f /vagrant/requirements.txt ]]; then
echo "Installing from ./requirements.txt with pip."
pip install -r /vagrant/requirements.txt
fi
echo "=== End Vagrant Provisioning using 'config/vagrant/virtualenv_setup.sh'"

9827
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"version": "1.0.0",
"repository": "https://github.com/nottinghamtec/PyRIGS",
"author": "Tom Price",
"license": "MIT",
"license": "Custom",
"dependencies": {
"@forevolve/bootstrap-dark": "^1.0.0-alpha.1075",
"@fortawesome/fontawesome-free": "^5.13.1",
@@ -16,8 +16,6 @@
"dark-mode-switch": "^1.0.0",
"flatpickr": "^4.6.6",
"fullcalendar": "^5.3.2",
"gulp-if": "^3.0.0",
"gulp-rename": "^2.0.0",
"jquery": "^3.5.1",
"jquery-ui-dist": "^1.12.1",
"konami": "^1.6.2",
@@ -30,7 +28,9 @@
"gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0",
"gulp-flatten": "^0.4.0",
"gulp-if": "^3.0.0",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^2.0.0",
"gulp-sass": "^4.1.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-terser": "^1.2.0",

View File

@@ -1,2 +1,3 @@
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
DJANGO_SETTINGS_MODULE = PyRIGS.settings
# FAIL_INVALID_TEMPLATE_VARS = True

View File

@@ -1,11 +1,11 @@
ansicolors==1.1.8
asgiref==3.2.10
asgiref==3.3.1
backports.tempfile==1.0
backports.weakref==1.0.post1
beautifulsoup4==4.9.3
cachetools==4.1.1
certifi==2020.6.20
chardet==3.0.4
cachetools==4.2.1
certifi==2020.12.5
chardet==4.0.0
configparser==5.0.1
contextlib2==0.6.0.post1
cssselect==1.1.0
@@ -13,8 +13,8 @@ cssutils==1.0.2
diff-match-patch==20200713
dj-database-url==0.5.0
dj-static==0.0.6
Django==3.1.2
django-debug-toolbar==3.1.1
Django==3.1.5
django-debug-toolbar==3.2
django-filter==2.4.0
django-gulp==4.1.0
django-ical==1.7.1
@@ -22,59 +22,59 @@ django-livereload==1.7
django-livereload-server==0.3.2
django-recaptcha==2.0.6
django-recurrence==1.10.3
django-registration-redux==2.8
django-reversion==3.0.8
django-registration-redux==2.9
django-reversion==3.0.9
django-toolbelt==0.0.1
django-widget-tweaks==1.4.8
envparse==0.2.0
env-tools==2.2.0
gunicorn==20.0.4
icalendar==4.0.7
idna==2.10
imgurpython==1.1.7
importlib-metadata==2.0.0
importlib-metadata==3.4.0
lxml==4.6.2
Markdown==3.3
msgpack==1.0.0
packaging==20.4
pep517==0.8.2
Pillow==7.2.0
Markdown==3.3.3
msgpack==1.0.2
packaging==20.8
pep517==0.9.1
Pillow==8.1.0
pluggy==0.13.1
premailer==3.7.0
progress==1.5
psutil==5.7.2
psutil==5.8.0
psycopg2==2.8.6
psycopg2-binary==2.8.6
Pygments==2.7.1
Pygments==2.7.4
pyparsing==2.4.7
PyPDF2==1.26.0
PyPOM==2.2.0
python-dateutil==2.8.1
pytoml==0.1.21
pytz==2020.1
pytz==2020.5
pytest-django==4.1.0
pytest-xdist==2.2.0
pytest-cov==2.11.1
raven==6.10.0
reportlab==3.5.53
requests==2.24.0
reportlab==3.5.60
requests==2.25.1
retrying==1.3.3
selenium==3.141.0
simplejson==3.17.2
six==1.15.0
soupsieve==2.0.1
soupsieve==2.1
sqlparse==0.4.1
static3==0.7.0
svg2rlg==0.3
tini==3.0.1
tornado==6.0.4
urllib3==1.25.10
tornado==6.1
urllib3==1.26.2
whitenoise==5.2.0
yolk==0.4.3
z3c.rml==3.10.0
zipp==3.3.0
z3c.rml==4.1.2
zipp==3.4.0
zope.component==4.6.2
zope.deferredimport==4.3.1
zope.deprecation==4.4.0
zope.event==4.5.0
zope.hookable==5.0.1
zope.interface==5.1.2
zope.interface==5.2.0
zope.proxy==4.3.5
zope.schema==6.0.0
zope.schema==6.0.1

View File

@@ -1 +1 @@
python-3.8.2
python-3.9.1

View File

@@ -305,6 +305,3 @@ class TestVersioningViews(TestCase):
response = self.client.get(request_url, follow=True)
self.assertContains(response, "Test Person")
self.assertEqual(response.status_code, 200)
# Functional Tests