Compare commits

..

1 Commits

Author SHA1 Message Date
ImgBotApp
d5dc879733 [ImgBot] Optimize images
*Total -- 8,936.02kb -> 7,990.11kb (10.59%)

/assets/static/imgs/square_logo.png -- 23.90kb -> 17.64kb (26.18%)
/RIGS/static/imgs/tappytaptap.gif -- 6,433.15kb -> 5,493.51kb (14.61%)
/RIGS/static/imgs/rigs.jpg -- 277.61kb -> 277.60kb (0%)
/RIGS/static/imgs/training.jpg -- 852.42kb -> 852.42kb (0%)
/RIGS/static/imgs/assets.jpg -- 1,348.94kb -> 1,348.94kb (0%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2022-01-18 19:32:05 +00:00
51 changed files with 631 additions and 674 deletions

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.6
WORKDIR /app
ADD . /app
RUN pip install -r requirements.txt && \
python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

View File

@@ -49,7 +49,7 @@ PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "*"
reportlab = "~=3.5.59"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
@@ -63,6 +63,7 @@ tornado = "~=6.1"
urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
"z3c.rml" = "~=4.1.2"
zipp = "~=3.4.0"
"zope.component" = "~=4.6.2"
"zope.deferredimport" = "~=4.3.1"
@@ -78,8 +79,6 @@ python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
"z3c.rml" = "*"
pikepdf = "*"
[dev-packages]
selenium = "~=3.141.0"

115
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "841781f4c4d3c12a34c0ff8ef3fd58171baf657478d5c339d4f6fc79d5830978"
"sha256": "7db5b3a9029be79c79efff791a42803a4765fe52c4f264f8a7be48ac4b1bda7a"
},
"pipfile-spec": 6,
"requires": {
@@ -73,7 +73,6 @@
},
"brotli": {
"hashes": [
"sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d",
"sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8",
"sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b",
"sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c",
@@ -83,13 +82,10 @@
"sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181",
"sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130",
"sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19",
"sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa",
"sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429",
"sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126",
"sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4",
"sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0",
"sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b",
"sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6",
"sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438",
"sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f",
"sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389",
@@ -101,13 +97,10 @@
"sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430",
"sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296",
"sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12",
"sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f",
"sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d",
"sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a",
"sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452",
"sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c",
"sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761",
"sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649",
"sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b",
"sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea",
"sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c",
@@ -117,23 +110,15 @@
"sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5",
"sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7",
"sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d",
"sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c",
"sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43",
"sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa",
"sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17",
"sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb",
"sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb",
"sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b",
"sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4",
"sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3",
"sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7",
"sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1",
"sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb",
"sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91",
"sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b",
"sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1",
"sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806",
"sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3",
"sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"
],
"index": "pypi",
@@ -401,10 +386,10 @@
},
"h11": {
"hashes": [
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
"version": "==0.13.0"
"version": "==0.12.0"
},
"html5lib": {
"hashes": [
@@ -431,12 +416,12 @@
},
"importlib-metadata": {
"hashes": [
"sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6",
"sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"
"sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
"sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
],
"index": "pypi",
"markers": "python_version < '3.10'",
"version": "==4.10.1"
"version": "==4.10.0"
},
"lxml": {
"hashes": [
@@ -604,7 +589,6 @@
"sha256:cd575cf0131683a7b661357bfd777b27c3c6c0d0fb7ef27e627f521122f75536",
"sha256:fb3d7fb390192cfb1e287503dbc03229c1c77fe9820cf084546bb63fa997fd87"
],
"index": "pypi",
"version": "==4.3.1"
},
"pillow": {
@@ -801,53 +785,38 @@
},
"reportlab": {
"hashes": [
"sha256:0430cfe397415759839ef89abee6db82e8a8f9bb5831a3c93e7763915c755345",
"sha256:13072e33e8cbac6fd6e776fecabdefafb0261886b2ab7cb3b874a9384f1b0ffe",
"sha256:1767106d03320e76a708d2c40488fe1785580a0d7abac7715e01a3cc910c1179",
"sha256:17f35a856bbf46989d557d4016822bcdd3ada88d3afb567de03a4b29676aa52e",
"sha256:19414f4357287a7573a60bcb76a092c9ea82bf09f01d04b3afb5c1bd3c660df2",
"sha256:1d8d9674eb6ba1b6c3d6a8e3d5d4e4231b3576db653d1b1fdac2538afee54c7a",
"sha256:23236dc70598b688e979444c4840c5cec88a2a12fe81ba6f8cc807120a2cad33",
"sha256:28c339d25eab804a8bd004dfaa5a80c7568178561741f4ce6e69dae05d38041f",
"sha256:2c93a551b60c7fd3b17942772847f7c4ee2f08ae74c87ef8f325fe8083d2aa6e",
"sha256:2e80045f36dd4b9b63b19fc073149f70857fe8590027ab3658db80ac6235ecd0",
"sha256:38aa912301d93e2267861d820cb3f6eebed8deb58d0df429421578b9ba033eee",
"sha256:47587ce01cf9ac25f6d187116a9f9cef710dc58ccea001024d950c4f5a504643",
"sha256:587b3d8ce0a065a00975516013aebb062e6161fba3cf399b22f270e4d9a3db1e",
"sha256:5a650284cc09caa32b5845c055bf035cb76949b87d57e9eed56d98f863613417",
"sha256:5e113c630b6109efe0285230706c8423bff1b82c2e2824e441401a467a1215b7",
"sha256:68e339411cc9329ff50982a7c1d55eabd53ac9be24d4442088af58328bae54d3",
"sha256:6ae1fb03faf4b6710e2c081d5208416a5d557e0cc00ff24fc124dd42a7158114",
"sha256:6f363e09aacaa7aaff232197fddb667d899822aa57d10091aea4fbb1f56b7fa7",
"sha256:70841d7eb4aa2f8ad4afacce07711481a0dcd9d01679da5627173443131a33a2",
"sha256:71d91002878c4d2a17a6bd7208c59373e6148977fe674bb79eec3eb9e63aa20f",
"sha256:7a09e5bf9c8e02c373e5e558cc5c2cfbc5d3c68560a406c6d16254363cfa989e",
"sha256:85095ef9f3697859064cb1b22f19659bf4ba25e7dadb9c6be65f322cd68ba88f",
"sha256:8dafdcdde7243f0864d6d11dd9bfffbd1e6bce6c3e668fe992f56ae48377c822",
"sha256:9a822486a98fe002bbe248fdf3f126739c1ad29032b54b71a3f67b6364a77677",
"sha256:aa57dc0818e066fdced9457b9e6c6fb269d63e2d96902001c7dbe010bce6ebcc",
"sha256:b0836c6cdee4b88e2366e0ff152c1327578149e09850b7cab6016444c5b3eb26",
"sha256:b2988ffc33032096e808e7a4a36f5b453fcc9587873c85c1b44bc6846bbbd09c",
"sha256:bd38d58895b359ef429df3c97dc00c3fef0ab57f45556de416ba9b7d7fc71ae2",
"sha256:be87dca9253efd3cd0f351b785530c02e67664e284e3c4a97cdd0c7dd806d39a",
"sha256:c21bdb11d7fccea28bf08eac13d9d031836e335c5e0620eae1d4336f193e9a03",
"sha256:c43f847f2598b5c2fc9b63871d7da641c0b90e384d8da8018d4d7173a0b82cd4",
"sha256:c780cc5208c67b25bdddd08480f874614cd0ec0bed39e1a848448543f2093945",
"sha256:c9bcf696bc8935ff90ecb50c7644e2af01f63a444d4b4bd39d41d2abdd7bb224",
"sha256:cb48b71088f5c9eff5715dde0bd4d5372d4713ffa92247acf0f04fd17ab2078d",
"sha256:d48f638893b3eb4c9b2afeec2de4f95a4b57fb8c398e3d7f9a7fb4b4d9546820",
"sha256:d8fe27ad312671c9347cf5997f7c1017833fac17233f33296281ba9fa0de189a",
"sha256:d98b759661070f5588b30152d0caaf16ac387f60372f8fa2568c9ad4014cd7f3",
"sha256:e2022ad36409e7616ed6311f7ab113f236cac66ba0d22be4f53bf7e77654b143",
"sha256:e45159f4d19304f5e79be13283fe53bdd006c4fd4d93ff3cb6ac082ca017c418",
"sha256:eb3ef5394b4b2c904ab467dbbe1efcfbe046e1395c2d3064420ccef89806570e",
"sha256:f326b04a3fb3c7c58b799bd23b60790b181893f052fe5a8011c9cd9984e24a43",
"sha256:f401ed014ea861dea2ae621f7810fb15b3bc021e6487dee97b32f175bbf1b7eb",
"sha256:f4d4eb3a949ccb0782e4d6560fcd5ee6f34636d1ee24f1d2a2b1f530af89481a",
"sha256:fdc3dc1242be557f6a8bb9e21751296cc721f60b8e2b684690049e656d798520"
"sha256:010f86a192c397f7c8ae667953a85d913395a8a6a8da112bff1c1ea28e679bcd",
"sha256:08b53568979228b6969b790339d06a0b8db8883f92ae7339013f9878042dd9ca",
"sha256:19708801278f600d712c04ee6bfb650e45d1b2898713f7bd97b39ab89bd08c1e",
"sha256:28c72d27f21d74a7301789c7950b5e82a430ed38817ecee060fa1f2f3e959360",
"sha256:2c0c88a7cf83a20a2bb355f97a1a9d0373a6de60c3aec35d301d3cc75dc4bb72",
"sha256:2dc5ee0c5b659697cdfbc218ec9abea54dd9c5a95ea8ca95245fe94f5ef111f9",
"sha256:332f836ff4c975c92d307302e86a54d6f0e3d2ce33a35759812e7a1d17e2091f",
"sha256:45113c1c359ba314499032c891487802cccd7c4225a3e930d6cf492d62ea4f07",
"sha256:46f15f5a34a50375c332ab8eaa907a0212c88787b0885ac25a9505c0741ee9ba",
"sha256:580eed6d9e5c20870ea909bec6840f9ceb9d13c33316d448cae21eb3ca47c7fd",
"sha256:5865c4247229584408515055b5b19c7f935ae94433d6258c7a9234c4a07d6d34",
"sha256:6063466779e438375bcdd2c15fc551ebd68f16ebfb2766497234df9cfa57e5b1",
"sha256:63578cab96fc4383e71dd9fe1877bb26ab78b2a6c91139068e99d130687289ab",
"sha256:66b5a08cbeb910edee7201efa786bd1bf7027c7ec526dddf7d60fc2252e2b30f",
"sha256:6b448a1824d381d282c5ea1da1669a5fa53dac67c57a1ecad6bcc149f286d1fd",
"sha256:6f905390f5e5801b21b6027c8ffaed915e5eec1e46bbdf6a74c8838213717b44",
"sha256:70e7461aa47eff810be8c4e4a0cbc6fcf47aecaddd46de6ca4524c76065f8490",
"sha256:7e466276f1a1121dac23b703af6c22db0cedf6cec5139969f8387e8d8046f203",
"sha256:81d1958d90fccf86f62b38ecbedf9208a973d99e0747b6cd75036914ae8641c4",
"sha256:9a00feb8eafbce1283cd3edbb29735bd40c9566b3f45913110a301700c16b63a",
"sha256:a48221d4ab7de37975ad052f7e565cf13ab708def63f203a38ae9927ab5442cd",
"sha256:ad9a49890de59e8dd16fa0ce03ef607e46a5ff2f39de44f8556f796b3d4ddffb",
"sha256:b25608059558910585a9e229bae0fd3d67af49ae5e1c7a20057680c6b3d5f6f7",
"sha256:b57ebeb28f7a58a9da6f8c293acb6d31d89f634b3eba0b728a040cef08afc4ea",
"sha256:b9ae0c534c09274b80f8fd87408071c1f814d56c5f51fe450b2157f1f13e921b",
"sha256:c0612d9101f40679245e7d9edb169d8d79378a47f38cd8e6b38c55d7ff31db3f",
"sha256:ced16daf89f948eeb4e376b5d814da5d99f7205fbd42e17a96f257e35dc31bdd",
"sha256:dd3409ebabe699c98058690b7b730f93e6b0bd4ed5e49ca3b15e1530ae07b40b",
"sha256:efef6a97e3ab49f3f40037dbf9a4166668a17cc6aaba13d5ecbabdf854a9b332"
],
"index": "pypi",
"version": "==3.6.5"
"version": "==3.5.68"
},
"requests": {
"hashes": [
@@ -872,11 +841,11 @@
},
"sentry-sdk": {
"hashes": [
"sha256:141da032f0fa4c56f9af6b361fda57360af1789576285bd1944561f9c274f9c0",
"sha256:9aeff2a47f4038460296b920bf4d269284e8454e1c67547ee002ccafd9c2442b"
"sha256:2cec50166bcb67e1965f8073541b2321e3864cd6fd42a526bcde9f0c4e4cc3f8",
"sha256:7bbaa32bba806ec629962f207b597e86831c7ee2c1f287c21ba7de7fea9a9c46"
],
"index": "pypi",
"version": "==1.5.3"
"version": "==1.5.2"
},
"simplejson": {
"hashes": [
@@ -1113,10 +1082,10 @@
},
"z3c.rml": {
"hashes": [
"sha256:11eca2ffe2e5db6d53b5a19617126db780afe779e11b5215651c8214aa98d8a6"
"sha256:0d730e2e61a29c69822ee955366f9d7e9a82e6909c11932329629fb0c1a128a0"
],
"index": "pypi",
"version": "==4.2.0"
"version": "==4.1.2"
},
"zipp": {
"hashes": [

View File

@@ -120,10 +120,10 @@ class TextBox(Region):
class SimpleMDETextArea(Region):
@property
def value(self):
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
def set_value(self, value):
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
class CheckBox(Region):
@@ -145,7 +145,7 @@ class RadioSelect(Region): # Currently only works for yes/no radio selects
value = "0"
else:
value = "1"
self.find_element(By.XPATH, f"//label[@for='{self.root.get_attribute('id')}_{value}']").click()
self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
@property
def value(self):

View File

@@ -2,12 +2,13 @@ import datetime
import operator
from functools import reduce
import simplejson
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
@@ -26,7 +27,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with
template_name = 'index.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(Index, self).get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count()
return context
@@ -38,7 +39,6 @@ class SecureAPIRequest(generic.View):
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'asset': asset_models.Asset,
'supplier': asset_models.Supplier,
'training_item': training_models.TrainingItem,
}
@@ -49,9 +49,8 @@ class SecureAPIRequest(generic.View):
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'asset': None,
'supplier': None,
'training_item': None,
'training_item': None, # TODO
}
'''
@@ -126,7 +125,8 @@ class SecureAPIRequest(generic.View):
results.append(data)
# return a data response
return JsonResponse(results, safe=False)
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
start = request.GET.get('start', None)
end = request.GET.get('end', None)
@@ -151,7 +151,8 @@ class SecureAPIRequest(generic.View):
}
results.append(data)
return JsonResponse(results, safe=False)
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
return HttpResponse(model)
@@ -175,7 +176,7 @@ class GenericListView(generic.ListView):
paginate_by = 20
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(GenericListView, self).get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
@@ -207,8 +208,8 @@ class GenericDetailView(generic.DetailView):
template_name = "generic_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
context = super(GenericDetailView, self).get_context_data(**kwargs)
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -218,8 +219,8 @@ class GenericUpdateView(generic.UpdateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Edit {self.model.__name__}"
context = super(GenericUpdateView, self).get_context_data(**kwargs)
context['page_title'] = "Edit {}".format(self.model.__name__)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -229,8 +230,8 @@ class GenericCreateView(generic.CreateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Create {self.model.__name__}"
context = super(GenericCreateView, self).get_context_data(**kwargs)
context['page_title'] = "Create {}".format(self.model.__name__)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -255,13 +256,14 @@ class CloseModal(generic.TemplateView):
class OEmbedView(generic.View):
def get(self, request, pk=None):
embed_url = reverse(self.url_name, args=[pk])
full_url = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}"
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': f'<iframe src="{full_url}" frameborder="0" width="100%" height="250"></iframe>',
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
return JsonResponse(data)
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")

View File

@@ -8,7 +8,6 @@ from django.db.models import Count
from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.db import IntegrityError
from reversion import revisions as reversion
from reversion.admin import VersionAdmin
@@ -22,11 +21,45 @@ admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin)
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
approve_user.short_description = "Approve selected users"
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin):
# Don't know how to add 'is_approved' whilst preserving the default list...
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = [approve_user]
class AssociateAdmin(VersionAdmin):
list_display = ('id', 'name', 'number_of_events')
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
merge_fields = ['name']
def get_queryset(self, request):
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
@@ -38,37 +71,17 @@ class AssociateAdmin(VersionAdmin):
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
master_object_pk = request.POST.get('master')
master_object = queryset.get(pk=master_object_pk)
masterObjectPk = request.POST.get('master')
masterObject = queryset.get(pk=masterObjectPk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
with transaction.atomic(), reversion.create_revision():
for obj in queryset.exclude(pk=master_object_pk):
# If we're merging profiles, merge their training information
if hasattr(obj, 'event_mic'):
events = obj.event_mic.all()
for event in events:
master_object.event_mic.add(event)
for qual in obj.qualifications_obtained.all():
try:
with transaction.atomic():
master_object.qualifications_obtained.add(qual)
except IntegrityError:
existing_qual = master_object.qualifications_obtained.get(item=qual.item, depth=qual.depth)
existing_qual.notes += qual.notes
existing_qual.save()
for level in obj.level_qualifications.all():
try:
with transaction.atomic():
master_object.level_qualifications.add(level)
except IntegrityError:
continue # Exists, oh well
else:
events = obj.event_set.all()
for event in events:
master_object.event_set.add(event)
for obj in queryset.exclude(pk=masterObjectPk):
events = obj.event_set.all()
for event in events:
masterObject.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
@@ -94,35 +107,6 @@ class AssociateAdmin(VersionAdmin):
return TemplateResponse(request, 'admin_associate_merge.html', context)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
list_display_links = ['username']
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = ['approve_user', 'merge']
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@admin.register(models.Person)
class PersonAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')

View File

@@ -17,7 +17,6 @@ from django.utils import timezone
from django.utils.functional import cached_property
from reversion import revisions as reversion
from reversion.models import Version
from versioning.versioning import RevisionMixin
class Profile(AbstractUser):
@@ -70,6 +69,39 @@ class Profile(AbstractUser):
return self.name
class RevisionMixin:
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
return len(versions) == 1
@property
def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first()
return version
@property
def last_edited_at(self):
version = self.current_version
if version is None:
return None
return version.revision.date_created
@property
def last_edited_by(self):
version = self.current_version
if version is None:
return None
return version.revision.user
@property
def current_version_id(self):
version = self.current_version
if version is None:
return None
return f"V{version.pk} | R{version.revision.pk}"
class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, default='')
@@ -569,7 +601,7 @@ class Invoice(models.Model, RevisionMixin):
@property
def activity_feed_string(self):
return f"#{self.display_id} for Event {self.event.display_id}"
return "#{} for Event {}".format(self.display_id, self.event.display_id)
def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
@@ -605,11 +637,11 @@ class Payment(models.Model, RevisionMixin):
reversion_hide = True
def __str__(self):
return f"{self.get_method_display()}: {self.amount}"
return "%s: %d" % (self.get_method_display(), self.amount)
@property
def activity_feed_string(self):
return f"payment of £{self.amount}"
return str("payment of £{}".format(self.amount))
def validate_url(value):

View File

@@ -27,7 +27,7 @@ from django.views import generic
from z3c.rml import rml2pdf
from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin
from PyRIGS.views import OEmbedView, is_ajax
from RIGS import models, forms
__author__ = 'ghost'
@@ -53,11 +53,10 @@ class WebCalendar(generic.TemplateView):
context = super().get_context_data(**kwargs)
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
# context['page_title'] = "Calendar"
return context
class EventDetail(generic.DetailView, ModalURLMixin):
class EventDetail(generic.DetailView):
template_name = 'event_detail.html'
model = models.Event
@@ -67,10 +66,6 @@ class EventDetail(generic.DetailView, ModalURLMixin):
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
return context
@@ -185,15 +180,11 @@ class EventPrint(generic.View):
merger = PdfFileMerger()
user_str = f"by {request.user.name} " if request.user is not None else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
context = {
'object': object,
'quote': True,
'current_user': request.user,
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date),
'info_string': f"[Paperwork generated {user_str}on {time} - {object.current_version_id}]",
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
}
rml = template.render(context)

View File

@@ -20,6 +20,8 @@
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
<script>
$(document).ready(function () {
$('button[data-action=add]').on('click', function (event) {

View File

@@ -8,13 +8,13 @@
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
<script src="{% static 'js/simplemde.min.js' %}"></script>
{% endblock %}
{% block js %}
@@ -23,6 +23,8 @@
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
<script>
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
$(document).ready(function () {

View File

@@ -106,7 +106,7 @@
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
</drawCenteredString>
</pageGraphics>
@@ -122,7 +122,7 @@
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="727"/>

View File

@@ -180,10 +180,15 @@
{% for item in object.items.all %}
<tr>
<td>
<para>{{ item.name }}</para>
{% if item.description %}
{{ item.description|markdown:"rml" }}
{% endif %}
<para>{{ item.name }}
{% if item.description %}
</para>
<para style="item_description">
{{ item.description|markdown:"rml" }}
</para>
<para>
{% endif %}
</para>
</td>
<td>£{{ item.cost|floatformat:2 }}</td>
<td>{{ item.quantity }}</td>
@@ -203,7 +208,9 @@
<tr>
<td>
{% if quote %}
<para>This quote is valid for 30 days unless otherwise arranged.</para>
<para>
This quote is valid for 30 days unless otherwise arranged.
</para>
{% endif %}
</td>
{% if object.vat > 0 %}

View File

@@ -9,7 +9,7 @@ register = template.Library()
@register.filter(name="markdown")
def markdown_filter(text, input_format='html', add_style=""):
def markdown_filter(text, input_format='html'):
# markdown library can't handle text=None
if text is None:
return text

View File

@@ -145,11 +145,11 @@ class CreateEvent(FormPage):
def add_person(self):
self.find_element(*self._add_person_selector).click()
return regions.Modal(self, self.driver.find_element(By.ID, 'modal'))
return regions.Modal(self, self.driver.find_element_by_id('modal'))
def add_event_item(self):
self.find_element(*self._add_item_selector).click()
element = self.driver.find_element(By.ID, 'itemModal')
element = self.driver.find_element_by_id('itemModal')
self.wait.until(EC.visibility_of(element))
return rigs_regions.ItemModal(self, element)

View File

@@ -1,155 +0,0 @@
An h1 header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
look like:
* this one
* that one
* the other one
Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
Unicode is supported.
An h2 header
------------
Here's a numbered list:
1. first item
2. second item
3. third item
Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here's a code sample:
# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:
~~~
define foobar() {
print "Welcome to flavor country!";
}
~~~
(which makes copying & pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:
~~~python
import time
# Quick, count to ten!
for i in range(10):
# (but not *too* quick)
time.sleep(0.5)
print i
~~~
### An h3 header ###
Now a nested list:
1. First, get these ingredients:
* carrots
* celery
* lentils
2. Boil some water.
3. Dump everything in the pot and follow
this algorithm:
find wooden spoon
uncover pot
stir
cover pot
balance wooden spoon precariously on pot handle
wait 10 minutes
goto first step (or shut off burner when done)
Do not bump wooden spoon or it will fall.
Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).
Here's a link to [a website](http://foo.bar). Here's a footnote [^1].
[^1]: Footnote text goes here.
Tables can look like this:
size material color
---- ------------ ------------
9 leather brown
10 hemp canvas natural
11 glass transparent
Table: Shoes, their sizes, and what they're made of
(The above is the caption for the table.) Pandoc also supports
multi-line tables:
-------- -----------------------
keyword text
-------- -----------------------
red Sunsets, apples, and
other red or reddish
things.
green Leaves, grass, frogs
and other things it's
not easy being.
-------- -----------------------
A horizontal rule follows.
***
Here's a definition list:
apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There's no "e" in tomatoe.
Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)
Here's a "line block":
| Line one
| Line too
| Line tree
and images can be specified like so:
![example image](example-image.jpg "An exemplary image")
Inline math equations go in like so: $\\omega = d\\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:
$$I = \\int \rho R^{2} dV$$
And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.

View File

@@ -1,8 +1,5 @@
import os
import pytest
from datetime import date
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase
from django.test.utils import override_settings
@@ -15,6 +12,8 @@ from pytest_django.asserts import assertRedirects, assertNotContains, assertCont
from PyRIGS.tests.base import assert_times_almost_equal, assert_oembed, login
from RIGS import models
import pytest
pytestmark = pytest.mark.django_db
@@ -372,7 +371,163 @@ def test_ra_redirect(admin_client, admin_user, ra):
class TestMarkdownTemplateTags(TestCase):
markdown = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read()
markdown = """
An h1 header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
look like:
* this one
* that one
* the other one
Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
Unicode is supported.
An h2 header
------------
Here's a numbered list:
1. first item
2. second item
3. third item
Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here's a code sample:
# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:
~~~
define foobar() {
print "Welcome to flavor country!";
}
~~~
(which makes copying & pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:
~~~python
import time
# Quick, count to ten!
for i in range(10):
# (but not *too* quick)
time.sleep(0.5)
print i
~~~
### An h3 header ###
Now a nested list:
1. First, get these ingredients:
* carrots
* celery
* lentils
2. Boil some water.
3. Dump everything in the pot and follow
this algorithm:
find wooden spoon
uncover pot
stir
cover pot
balance wooden spoon precariously on pot handle
wait 10 minutes
goto first step (or shut off burner when done)
Do not bump wooden spoon or it will fall.
Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).
Here's a link to [a website](http://foo.bar). Here's a footnote [^1].
[^1]: Footnote text goes here.
Tables can look like this:
size material color
---- ------------ ------------
9 leather brown
10 hemp canvas natural
11 glass transparent
Table: Shoes, their sizes, and what they're made of
(The above is the caption for the table.) Pandoc also supports
multi-line tables:
-------- -----------------------
keyword text
-------- -----------------------
red Sunsets, apples, and
other red or reddish
things.
green Leaves, grass, frogs
and other things it's
not easy being.
-------- -----------------------
A horizontal rule follows.
***
Here's a definition list:
apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There's no "e" in tomatoe.
Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)
Here's a "line block":
| Line one
| Line too
| Line tree
and images can be specified like so:
![example image](example-image.jpg "An exemplary image")
Inline math equations go in like so: $\\omega = d\\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:
$$I = \\int \rho R^{2} dV$$
And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.
"""
def test_html_safe(self):
html = markdown_filter(self.markdown)
@@ -401,7 +556,6 @@ class TestMarkdownTemplateTags(TestCase):
description=self.markdown,
start_date='2016-01-01',
)
event_item = models.EventItem.objects.create(event=event, name="TI I1", quantity=1, cost=1.00, order=1, description="* test \n * test \n * test")
user = models.Profile.objects.create(
username='RML test',
is_superuser=True, # Don't care about permissions

View File

@@ -5,7 +5,7 @@ from django.views.generic import RedirectView
from PyRIGS.decorators import (api_key_required, has_oembed,
permission_required_with_403)
from . import views
from RIGS import finance, ical, rigboard, views, hs
urlpatterns = [
# People
@@ -42,101 +42,101 @@ urlpatterns = [
name='venue_update'),
# Rigboard
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
path('rigboard/', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(rigboard.WebCalendar.as_view()),
name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
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()(views.WebCalendar.as_view()), name='web_calendar'),
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(rigboard.EventDetail.as_view()),
name='event_detail'),
path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()),
path('event/create/', permission_required_with_403('RIGS.add_event')(rigboard.EventCreate.as_view()),
name='event_create'),
path('event/archive/', login_required()(views.EventArchive.as_view()),
path('event/archive/', login_required()(rigboard.EventArchive.as_view()),
name='event_archive'),
path('event/<int:pk>/embed/',
xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())),
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
name='event_embed'),
path('event/<int:pk>/oembed_json/', views.EventOEmbed.as_view(),
path('event/<int:pk>/oembed_json/', rigboard.EventOEmbed.as_view(),
name='event_oembed'),
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(views.EventPrint.as_view()),
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
name='event_print'),
path('event/<int:pk>/edit/', permission_required_with_403('RIGS.change_event')(views.EventUpdate.as_view()),
path('event/<int:pk>/edit/', permission_required_with_403('RIGS.change_event')(rigboard.EventUpdate.as_view()),
name='event_update'),
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(rigboard.EventDuplicate.as_view()),
name='event_duplicate'),
# Event H&S
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(hs.HSList.as_view()), name='hs_list'),
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(hs.EventRiskAssessmentCreate.as_view()),
name='event_ra'),
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentDetail.as_view()),
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(hs.EventRiskAssessmentDetail.as_view()),
name='ra_detail'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(hs.EventRiskAssessmentEdit.as_view()),
name='ra_edit'),
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentList.as_view()),
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(hs.EventRiskAssessmentList.as_view()),
name='ra_list'),
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(hs.EventRiskAssessmentReview.as_view()),
name='ra_review'),
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(hs.EventChecklistCreate.as_view()),
name='event_ec'),
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistDetail.as_view()),
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(hs.EventChecklistDetail.as_view()),
name='ec_detail'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(hs.EventChecklistEdit.as_view()),
name='ec_edit'),
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistList.as_view()),
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(hs.EventChecklistList.as_view()),
name='ec_list'),
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.EventChecklistReview.as_view()),
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(hs.EventChecklistReview.as_view()),
name='ec_review'),
# Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
path('invoice/', permission_required_with_403('RIGS.view_invoice')(finance.InvoiceIndex.as_view()),
name='invoice_list'),
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceArchive.as_view()),
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(finance.InvoiceArchive.as_view()),
name='invoice_archive'),
path('invoice/waiting/', permission_required_with_403('RIGS.add_invoice')(views.InvoiceWaiting.as_view()),
path('invoice/waiting/', permission_required_with_403('RIGS.add_invoice')(finance.InvoiceWaiting.as_view()),
name='invoice_waiting'),
path('event/<int:pk>/invoice/', permission_required_with_403('RIGS.add_invoice')(views.InvoiceEvent.as_view()),
path('event/<int:pk>/invoice/', permission_required_with_403('RIGS.add_invoice')(finance.InvoiceEvent.as_view()),
name='invoice_event'),
path('event/<int:pk>/invoice/void', permission_required_with_403('RIGS.add_invoice')(views.InvoiceEvent.as_view()),
path('event/<int:pk>/invoice/void', permission_required_with_403('RIGS.add_invoice')(finance.InvoiceEvent.as_view()),
name='invoice_event_void', kwargs={'void': True}),
path('invoice/<int:pk>/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceDetail.as_view()),
path('invoice/<int:pk>/', permission_required_with_403('RIGS.view_invoice')(finance.InvoiceDetail.as_view()),
name='invoice_detail'),
path('invoice/<int:pk>/print/', permission_required_with_403('RIGS.view_invoice')(views.InvoicePrint.as_view()),
path('invoice/<int:pk>/print/', permission_required_with_403('RIGS.view_invoice')(finance.InvoicePrint.as_view()),
name='invoice_print'),
path('invoice/<int:pk>/void/', permission_required_with_403('RIGS.change_invoice')(views.InvoiceVoid.as_view()),
path('invoice/<int:pk>/void/', permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
name='invoice_void'),
path('invoice/<int:pk>/delete/',
permission_required_with_403('RIGS.change_invoice')(views.InvoiceDelete.as_view()),
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceDelete.as_view()),
name='invoice_delete'),
path('payment/create/', permission_required_with_403('RIGS.add_payment')(views.PaymentCreate.as_view()),
path('payment/create/', permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
name='payment_create'),
path('payment/<int:pk>/delete/', permission_required_with_403('RIGS.add_payment')(views.PaymentDelete.as_view()),
path('payment/<int:pk>/delete/', permission_required_with_403('RIGS.add_payment')(finance.PaymentDelete.as_view()),
name='payment_delete'),
# Client event authorisation
path('event/<pk>/auth/',
permission_required_with_403('RIGS.change_event')(views.EventAuthorisationRequest.as_view()),
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthorisationRequest.as_view()),
name='event_authorise_request'),
path('event/<int:pk>/auth/preview/',
permission_required_with_403('RIGS.change_event')(views.EventAuthoriseRequestEmailPreview.as_view()),
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthoriseRequestEmailPreview.as_view()),
name='event_authorise_preview'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', views.EventAuthorise.as_view(),
re_path(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]+)/preview/$', views.EventAuthorise.as_view(preview=True),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/preview/$', rigboard.EventAuthorise.as_view(preview=True),
name='event_authorise_form_preview'),
# ICS Calendar - API key authentication
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(views.CalendarICS()),
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
name="ics_calendar"),

View File

@@ -1,5 +0,0 @@
from .crud import *
from .finance import *
from .hs import *
from .ical import *
from .rigboard import *

View File

@@ -189,7 +189,3 @@ class Asset(models.Model, RevisionMixin):
@property
def display_id(self):
return str(self.asset_id)
@property
def name(self):
return f"{self.display_id} | {self.description}"

View File

@@ -5,13 +5,13 @@
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
<script src="{% static 'js/simplemde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %}
@@ -31,8 +31,48 @@
checkIfCableHidden();
</script>
<script>
$('document').ready(function(){
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
$('#parent_id')
.selectpicker({
liveSearch: true
})
.ajaxSelectPicker({
ajax: {
url: "{% url 'asset_search_json' %}",
type: "GET",
data: function () {
let params = {
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
};
return params;
}
},
locale: {
emptyTitle: 'Search for item...'
},
preprocessData: function(data){
var assets = [];
if(data.length){
var len = data.length;
for(var i = 0; i < len; i++){
var curr = data[i];
assets.push(
{
'value': curr.id,
'text': curr.label,
'disabled': false
}
);
}
assets.push(
{
'value': null,
'text': "No parent"
});
}
return assets;
},
preserveSelected: false
});
</script>
<script>

View File

@@ -7,7 +7,7 @@
{% if create or edit or duplicate %}
<div class="form-group" id="parent-group">
<label for="selectpicker">Set Parent</label>
<select name="parent" id="parent_id" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='asset' %}?fields=asset_id,description">
<select name="parent" id="parent_id" class="form-control selectpicker" data-live-search="true">
{% if object.parent %}
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
{% endif %}

View File

@@ -1,4 +1,3 @@
import time
import datetime
from django.utils import timezone
@@ -139,11 +138,11 @@ class TestAssetForm(AutoLoginTest):
self.page.parent_selector.toggle()
self.assertTrue(self.page.parent_selector.is_open)
option = self.parent.asset_id
option = str(self.parent)
self.page.parent_selector.search(option)
time.sleep(2) # Slow down for javascript
# self.page.parent_selector.set_option(option, True)
# self.assertTrue(self.page.parent_selector.options[0].selected)
self.driver.implicitly_wait(1)
self.page.parent_selector.set_option(option, True)
self.assertTrue(self.page.parent_selector.options[0].selected)
self.page.parent_selector.toggle()
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())

View File

@@ -27,6 +27,7 @@ urlpatterns = [
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
path('cabletype/<int:pk>/detail/', login_required(views.CableTypeDetail.as_view()), name='cable_type_detail'),
path('asset/search/', login_required(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())),
@@ -42,4 +43,6 @@ 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/search/', login_required(views.SupplierSearch.as_view()), name='supplier_search_json'),
]

View File

@@ -87,10 +87,28 @@ class AssetList(LoginRequiredMixin, generic.ListView):
return context
class AssetSearch(AssetList):
hide_hidden_status = False
def render_to_response(self, context, **response_kwargs):
result = []
for asset in context["object_list"]:
result.append({"id": asset.pk, "label": (asset.asset_id + " | " + asset.description)})
return JsonResponse(result, safe=False)
class AssetIDUrlMixin:
def get_object(self, queryset=None):
pk = self.kwargs.get(self.pk_url_kwarg)
return get_object_or_404(models.Asset, asset_id=pk)
queryset = models.Asset.objects.filter(asset_id=pk)
try:
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404("No assets found matching the query")
return obj
class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView):
@@ -176,6 +194,7 @@ class AssetOEmbed(OEmbedView):
class AssetAuditList(AssetList):
template_name = 'asset_audit_list.html'
hide_hidden_status = False
# TODO Refresh this when the modal is submitted
def get_queryset(self):
@@ -223,6 +242,17 @@ class SupplierList(GenericListView):
return context
class SupplierSearch(SupplierList):
hide_hidden_status = False
def render_to_response(self, context, **response_kwargs):
result = []
for supplier in context["object_list"]:
result.append({"id": supplier.pk, "name": supplier.name})
return JsonResponse(result, safe=False)
class SupplierDetail(GenericDetailView):
model = models.Supplier

View File

@@ -27,7 +27,8 @@ function styles(done) {
'node_modules/fullcalendar/main.css',
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
'node_modules/easymde/dist/easymde.min.css'
'node_modules/flatpickr/dist/flatpickr.css',
'node_modules/simplemde/dist/simplemde.min.css'
])
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
@@ -58,11 +59,12 @@ function scripts() {
'node_modules/html5sortable/dist/html5sortable.min.js',
'node_modules/clipboard/dist/clipboard.min.js',
'node_modules/flatpickr/dist/flatpickr.min.js',
'node_modules/moment/moment.js',
'node_modules/fullcalendar/main.js',
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
'node_modules/easymde/dist/easymde.min.js',
'node_modules/simplemde/dist/simplemde.min.js',
'node_modules/konami/konami.js',
'pipeline/source_assets/js/**/*.js',])
.pipe(gulpif(function(file) { return base_scripts.includes(file.relative);}, con('base.js')))

185
package-lock.json generated
View File

@@ -13,11 +13,11 @@
"ajax-bootstrap-select": "^1.4.5",
"autocompleter": "^6.1.2",
"autoprefixer": "^10.4.0",
"bootstrap": "^4.6.1",
"bootstrap": "^4.5.2",
"bootstrap-select": "^1.13.17",
"clipboard": "^2.0.8",
"cssnano": "^5.0.13",
"easymde": "^2.16.1",
"flatpickr": "^4.6.6",
"fullcalendar": "^5.10.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
@@ -34,6 +34,7 @@
"node-sass": "^7.0.0",
"popper.js": "^1.16.1",
"postcss": "^8.4.5",
"simplemde": "^1.11.2",
"uglify-js": "^3.14.5"
},
"devDependencies": {
@@ -255,24 +256,6 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/codemirror": {
"version": "5.60.5",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
"integrity": "sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw=="
},
"node_modules/@types/marked": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.1.tgz",
"integrity": "sha512-ZigEmCWdNUU7IjZEuQ/iaimYdDHWHfTe3kg8ORfKjyGYd9RWumPoOJRQXB0bO+XLkNwzCthW3wUIQtANaEZ1ag=="
},
"node_modules/@types/minimist": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
@@ -283,14 +266,6 @@
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw=="
},
"node_modules/@types/tern": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -957,17 +932,9 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"node_modules/bootstrap": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.1"
}
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw=="
},
"node_modules/bootstrap-select": {
"version": "1.13.18",
@@ -1612,20 +1579,12 @@
}
},
"node_modules/copy-props": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz",
"integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz",
"integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==",
"dependencies": {
"each-props": "^1.3.2",
"is-plain-object": "^5.0.0"
}
},
"node_modules/copy-props/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
"each-props": "^1.3.0",
"is-plain-object": "^2.0.1"
}
},
"node_modules/core-util-is": {
@@ -2067,18 +2026,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/easymde": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.16.1.tgz",
"integrity": "sha512-FihYgjRsKfhGNk89SHSqxKLC4aJ1kfybPWW6iAmtb5GnXu+tnFPSzSaGBmk1RRlCuhFSjhF0SnIMGVPjEzkr6g==",
"dependencies": {
"@types/codemirror": "^5.60.4",
"@types/marked": "^4.0.1",
"codemirror": "^5.63.1",
"codemirror-spell-checker": "1.1.2",
"marked": "^4.0.10"
}
},
"node_modules/eazy-logger": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.1.0.tgz",
@@ -2788,6 +2735,11 @@
"node": ">= 0.10"
}
},
"node_modules/flatpickr": {
"version": "4.6.9",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.9.tgz",
"integrity": "sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw=="
},
"node_modules/flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -2798,23 +2750,12 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
"version": "1.14.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
"integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-in": {
@@ -4927,9 +4868,9 @@
}
},
"node_modules/marked": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.8.tgz",
"integrity": "sha512-dkpJMIlJpc833hbjjg8jraw1t51e/eKDoG8TFOgc5O0Z77zaYKigYekTDop5AplRoKFGIaoazhYEhGkMtU3IeA==",
"bin": {
"marked": "bin/marked.js"
},
@@ -5500,9 +5441,9 @@
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"node_modules/nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
"version": "3.1.30",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
"integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -7377,6 +7318,16 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
},
"node_modules/simplemde": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/simplemde/-/simplemde-1.11.2.tgz",
"integrity": "sha1-ojo12XjSxA7wfewAjJLwcNjggOM=",
"dependencies": {
"codemirror": "*",
"codemirror-spell-checker": "*",
"marked": "*"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -9162,24 +9113,6 @@
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
},
"@types/codemirror": {
"version": "5.60.5",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
"integrity": "sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==",
"requires": {
"@types/tern": "*"
}
},
"@types/estree": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw=="
},
"@types/marked": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.1.tgz",
"integrity": "sha512-ZigEmCWdNUU7IjZEuQ/iaimYdDHWHfTe3kg8ORfKjyGYd9RWumPoOJRQXB0bO+XLkNwzCthW3wUIQtANaEZ1ag=="
},
"@types/minimist": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
@@ -9190,14 +9123,6 @@
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw=="
},
"@types/tern": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
"requires": {
"@types/estree": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -9709,10 +9634,9 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"bootstrap": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==",
"requires": {}
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw=="
},
"bootstrap-select": {
"version": "1.13.18",
@@ -10627,18 +10551,6 @@
"lodash": "^4.17.10"
}
},
"easymde": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.16.1.tgz",
"integrity": "sha512-FihYgjRsKfhGNk89SHSqxKLC4aJ1kfybPWW6iAmtb5GnXu+tnFPSzSaGBmk1RRlCuhFSjhF0SnIMGVPjEzkr6g==",
"requires": {
"@types/codemirror": "^5.60.4",
"@types/marked": "^4.0.1",
"codemirror": "^5.63.1",
"codemirror-spell-checker": "1.1.2",
"marked": "^4.0.10"
}
},
"eazy-logger": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.1.0.tgz",
@@ -11251,6 +11163,11 @@
"resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
"integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q=="
},
"flatpickr": {
"version": "4.6.9",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.9.tgz",
"integrity": "sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw=="
},
"flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -13431,9 +13348,9 @@
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA=="
"version": "3.1.30",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
"integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ=="
},
"nanomatch": {
"version": "1.2.13",
@@ -14893,6 +14810,16 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
},
"simplemde": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/simplemde/-/simplemde-1.11.2.tgz",
"integrity": "sha1-ojo12XjSxA7wfewAjJLwcNjggOM=",
"requires": {
"codemirror": "*",
"codemirror-spell-checker": "*",
"marked": "*"
}
},
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",

View File

@@ -10,11 +10,11 @@
"ajax-bootstrap-select": "^1.4.5",
"autocompleter": "^6.1.2",
"autoprefixer": "^10.4.0",
"bootstrap": "^4.6.1",
"bootstrap": "^4.5.2",
"bootstrap-select": "^1.13.17",
"clipboard": "^2.0.8",
"cssnano": "^5.0.13",
"easymde": "^2.16.1",
"flatpickr": "^4.6.6",
"fullcalendar": "^5.10.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
@@ -31,6 +31,7 @@
"node-sass": "^7.0.0",
"popper.js": "^1.16.1",
"postcss": "^8.4.5",
"simplemde": "^1.11.2",
"uglify-js": "^3.14.5"
},
"devDependencies": {

View File

@@ -1,3 +1,7 @@
marked.setOptions({
breaks: true,
})
function setupItemTable(items_json) {
objectitems = JSON.parse(items_json)
$.each(objectitems, function (key, val) {
@@ -33,8 +37,7 @@ function updatePrices() {
}
function setupMDE(selector) {
editor = new EasyMDE({
autoDownloadFontAwesome: false,
editor = new SimpleMDE({
element: $(selector)[0],
forceSync: true,
toolbar: ["bold", "italic", "strikethrough", "|", "unordered-list", "ordered-list", "|", "link", "|", "preview", "guide"],
@@ -117,7 +120,7 @@ $('body').on('submit', '#item-form', function (e) {
// update the table
$row = $('#item-' + pk);
$row.find('.name').html(escapeHtml(fields.name));
$row.find('.description').html(fields.description);
$row.find('.description').html(marked(fields.description));
$row.find('.cost').html(parseFloat(fields.cost).toFixed(2));
$row.find('.quantity').html(fields.quantity);

View File

@@ -123,7 +123,7 @@
color: $gray-100;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
@@ -145,7 +145,7 @@
.editor-toolbar > a.active {
background: $info !important;
}
.cm-s-easymde {
.cm-s-paper {
color: white;
background-color: $darktheme;
border-color: #bbb;
@@ -153,7 +153,4 @@
.CodeMirror-cursor {
border-color: white !important;
}
.modal {
overflow-y: auto !important; //Bootstrap Dark Theme overrides this to none for some insane reason so we need to change it back
}
}

View File

@@ -116,6 +116,10 @@ textarea {
hyphens: auto;
}
.modal-dialog {
z-index: inherit; // bug fix introduced in 52682ce
}
del {
background-color: #f2dede;
border-radius: 3px;

21
templates/analytics.html Normal file
View File

@@ -0,0 +1,21 @@
{% if not debug %}
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-43285686-12', 'auto');
{% if user.is_authenticated %}
ga('set', '&uid', {{ user.pk }});
{% endif %}
ga('require', 'linkid', 'linkid.js');
ga('send', 'pageview');
</script>
{% endif %}

View File

@@ -29,6 +29,7 @@
<body>
<a class="skip-link" href='#main'>Skip to content</a>
{% include "analytics.html" %}
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
<div class="container">
@@ -78,6 +79,7 @@
<div class="modal fade" id="modal" role="dialog" tabindex=-1></div>
<script src="{% static 'js/base.js' %}"></script>
<script src="{% static 'js/marked.min.js' %}"></script>
{% include 'partials/dark_theme.html' %}
{% block js %}

View File

@@ -15,6 +15,7 @@
</head>
<body>
{% include "analytics.html" %}
<div class="embed_container">
<div class="container-fluid">
{% if messages %}

View File

@@ -10,7 +10,7 @@
<h2 class="col-sm-12 pb-3">Welcome back {{ user.get_full_name }}, there {%if rig_count == 1 %}is one rig coming up{%else%}are {{ rig_count|apnumber }} rigs coming up.{%endif%}</h2>
<div class="col-sm-4 mb-3">
<div class="card">
<img class="card-img-top d-none d-sm-block" src="{% static 'imgs/rigs.jpg' %}" alt="Some lights and haze, very purple" style="height: 150px; object-fit: cover;">
<img class="card-img-top" src="{% static 'imgs/rigs.jpg' %}" alt="" style="height: 150px; object-fit: cover;">
<h4 class="card-header">Rigboard</h4>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="{% url 'rigboard' %}"><span class="fas fa-list align-middle"></span><span class="align-middle"> Rigboard</span></a>
@@ -24,7 +24,7 @@
<div class="col-sm-4 mb-3">
<div class="card">
{% now "m-d" as todays_date %}
<img class="card-img-top d-none d-sm-block" src="{% if todays_date == '04-01' %}{% static 'imgs/tappytaptap.gif' %}{%else%}{% static 'imgs/assets.jpg' %}{%endif%}" alt="M32 sound desk close up of the faders" style="height: 150px; object-fit: cover;">
<img class="card-img-top" src="{% if todays_date == '04-01' %}{% static 'imgs/tappytaptap.gif' %}{%else%}{% static 'imgs/assets.jpg' %}{%endif%}" alt="" style="height: 150px; object-fit: cover;">
<h4 class="card-header">Asset Database</h4>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="{% url 'asset_index' %}"><span class="fas fa-tag align-middle"></span><span class="align-middle"> Asset List</span></a>
@@ -40,7 +40,7 @@
</div>
<div class="col-sm-4 mb-3">
<div class="card">
<img class="card-img-top d-none d-sm-block" src="{% static 'imgs/training.jpg' %}" alt="People watching a presentation" style="height: 150px; object-fit: cover;">
<img class="card-img-top" src="{% static 'imgs/training.jpg' %}" alt="" style="height: 150px; object-fit: cover;">
<h4 class="card-header">Training Database</h4>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action text-info" href="{% url 'trainee_detail' request.user.pk %}"><span class="fas fa-file-signature align-middle"></span><span class="align-middle"> My Training Record</span></a>

View File

@@ -1,7 +1,7 @@
{% load static %}
<script>
if({{ request.user.dark_theme|lower|default:'false' }} || window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.querySelector('head').innerHTML += '<link rel="stylesheet" href="{% static "css/dark_screen.css" %}" type="text/css"/>';
$('<link>').prependTo('head').attr({type : 'text/css', rel : 'stylesheet'}).attr('href', '{% static "css/dark_screen.css" %}');
document.body.setAttribute('data-theme', 'dark');
}
</script>

View File

@@ -0,0 +1,34 @@
{% load static %}
<script>
function initDatetime() {
$('input[type=datetime-local]').not(':disabled').flatpickr({
dateFormat: 'Y-m-dTH:m',
enableTime: true,
allowInput: true,
altInput: true,
altFormat: "d/m/y H:m",
});
}
$(document).ready(function () {
function supportsDateTime() {
var input = document.createElement('input');
input.setAttribute('type','datetime-local');
return input.type !== "text";
}
//Firefox reports support for datetime-local without properly supporting it. Bah.
if(!supportsDateTime() || navigator.userAgent.toLowerCase().indexOf('firefox') > -1){
$('<link>')
.appendTo('head')
.attr({type : 'text/css', rel : 'stylesheet'})
.attr('href', '{% static "css/flatpickr.css" %}');
$.when(
$.getScript( '{% static "js/flatpickr.min.js" %}' ),
$.Deferred(function(deferred){
$(deferred.resolve);
})
).done(function(){
initDatetime();
});
}
});
</script>

View File

@@ -23,13 +23,9 @@ class QualificationForm(forms.ModelForm):
def clean_supervisor(self):
supervisor = self.cleaned_data['supervisor']
item = self.cleaned_data['item']
if supervisor.pk == self.cleaned_data['trainee'].pk:
raise forms.ValidationError('One may not supervise oneself...')
if item.category.training_level:
if not supervisor.level_qualifications.filter(level=item.category.training_level):
raise forms.ValidationError('Selected supervising person is missing requisite training level to train in this department')
elif not supervisor.is_supervisor:
if not supervisor.is_supervisor:
raise forms.ValidationError('Selected supervisor must actually *be* a supervisor...')
return supervisor

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.11 on 2022-01-25 12:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('training', '0002_alter_traininglevel_options'),
]
operations = [
migrations.AddField(
model_name='trainingcategory',
name='training_level',
field=models.ForeignKey(help_text='If this is set, any user with the selected level may pass out users within this category, regardless of other status', null=True, on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel'),
),
]

View File

@@ -1,47 +1,35 @@
from RIGS.models import Profile
from RIGS.models import RevisionMixin, Profile
from reversion import revisions as reversion
from django.db import models
from django.urls import reverse
from django.utils.safestring import mark_safe
from versioning.versioning import RevisionMixin
class TraineeManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True, is_approved=True)
@reversion.register(for_concrete_model=False, fields=[])
@reversion.register(for_concrete_model=False, fields=[], follow=["qualifications_obtained", "level_qualifications"])
class Trainee(Profile, RevisionMixin):
class Meta:
proxy = True
objects = TraineeManager()
# FIXME use queryset
def started_levels(self):
return [level for level in TrainingLevel.objects.all() if level.percentage_complete(self) > 0 and level.pk not in self.level_qualifications.values_list('level', flat=True)]
@property
def confirmed_levels(self):
return self.level_qualifications.exclude(confirmed_on=None).select_related('level')
@property
def is_technician(self):
return self.confirmed_levels \
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
.filter(level__level=TrainingLevel.TECHNICIAN) \
.exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists()
@property
def is_driver(self):
return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists()
return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level').filter(level__department=TrainingLevel.HAULAGE).exists()
def get_records_of_depth(self, depth):
return self.qualifications_obtained.filter(depth=depth).select_related('item', 'trainee', 'supervisor')
def is_user_qualified_in(self, item, required_depth):
return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).exists()
return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).first() is not None # this is a somewhat ghetto version of get_or_none
def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.pk})
@@ -50,15 +38,10 @@ class Trainee(Profile, RevisionMixin):
def display_id(self):
return str(self)
@property
def full_name(self):
return self.first_name + " " + self.last_name
class TrainingCategory(models.Model):
reference_number = models.IntegerField(unique=True)
name = models.CharField(max_length=50)
training_level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE, null=True, help_text="If this is set, any user with the selected level may pass out users within this category, regardless of other status")
def __str__(self):
return f"{self.reference_number}. {self.name}"
@@ -113,14 +96,14 @@ class TrainingItemQualification(models.Model, RevisionMixin):
# TODO Maximum depth - some things stop at Complete and you can't be passed out in them
def __str__(self):
return f"{self.get_depth_display()} in {self.item} on {self.date.strftime('%b %d %Y')}"
return "{} in {} on {}".format(self.get_depth_display(), self.item, self.date.strftime("%b %d %Y"))
@property
def activity_feed_string(self):
return f"{self.get_depth_display()} in {self.item}"
return str("{} in {}".format(self.get_depth_display(), self.item))
@classmethod
def get_colour_from_depth(cls, depth):
def get_colour_from_depth(cls, obj, depth):
if depth == 0:
return "warning"
if depth == 1:
@@ -255,8 +238,7 @@ class TrainingLevelRequirement(models.Model, RevisionMixin):
reversion_hide = True
def __str__(self):
depth = TrainingItemQualification.CHOICES[self.depth][1]
return f"{depth} in {self.item}"
return "{} in {}".format(TrainingItemQualification.CHOICES[self.depth][1], self.item)
class Meta:
unique_together = ["level", "item"]

View File

@@ -42,7 +42,7 @@
</div>
<div class="form-group form-row">
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required>
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials&filters=is_supervisor" required>
{% if object.supervisor %}
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
{% endif %}

View File

@@ -16,17 +16,7 @@
{% endblock %}
{% block content %}
<form method="GET" class="ml-auto w-25 d-flex flex-column justify-content-end">
{% csrf_token %}
<div class="input-group">
<input type="search" name="q" placeholder="Search" value="{{ request.GET.q }}"
class="form-control" id="id_search_text"/>
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
</div>
<button type="submit" class="btn btn-primary mt-2 {% if request.GET.is_supervisor %}active{%endif%}" data-toggle="button" aria-pressed="{% if request.GET.is_supervisor %}true{%endif%}" name="is_supervisor" value="{% if request.GET.is_supervisor %}{% else %}True{% endif %}">
Only Supervisors
</button>
</form>
{% include 'partials/list_search.html' %}
<div class="row pt-2">
<div class="col">
<div class="table-responsive">

View File

@@ -35,7 +35,7 @@ def colour_from_depth(depth):
@register.filter
def get_levels_of_depth(trainee, level):
return trainee.level_qualifications.all().exclude(confirmed_on=None).exclude(level__department=None).exclude(level__department=models.TrainingLevel.HAULAGE).select_related('level').filter(level__level=level)
return trainee.level_qualifications.all().exclude(confirmed_on=None).exclude(level__department=models.TrainingLevel.HAULAGE).select_related('level').filter(level__level=level)
@register.simple_tag

View File

@@ -4,10 +4,9 @@ import pytest
from django.utils import timezone
from django.urls import reverse
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains, assertURLEqual
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
from training import models
from reversion.models import Version, Revision
def test_add_qualification(admin_client, trainee, admin_user):
@@ -20,22 +19,6 @@ def test_add_qualification(admin_client, trainee, admin_user):
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
def test_add_qualification_reversion(admin_client, trainee, training_item, supervisor):
url = reverse('add_qualification', kwargs={'pk': trainee.pk})
date = (timezone.now() + datetime.timedelta(days=-3)).strftime("%Y-%m-%d")
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 0, 'notes': ""})
print(response.content)
assert response.status_code == 302
qual = models.TrainingItemQualification.objects.last()
assert qual is not None
assert training_item.pk == qual.pk
# Ensure only one revision has been created
assert Revision.objects.count() == 1
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})
assert Revision.objects.count() == 2
assert Version.objects.count() == 4 # Two item qualifications and the trainee twice
def test_add_requirement(admin_client, level):
url = reverse('add_requirement', kwargs={'pk': level.pk})
response = admin_client.post(url)
@@ -53,10 +36,3 @@ def test_trainee_detail(admin_client, trainee, admin_user):
assertNotContains(response, "Your")
name = trainee.first_name + " " + trainee.last_name
assertContains(response, f"{name}'s Training Record")
def test_trainee_item_detail(admin_client, trainee):
url = reverse('trainee_item_detail', kwargs={'pk': trainee.pk})
response = admin_client.get(url)
assert response.status_code == 200
assertContains(response, "Nothing found")

View File

@@ -9,7 +9,6 @@ from django.db.models import Q, Count
from PyRIGS.views import is_ajax, ModalURLMixin
from training import models, forms
from users import views
from reversion.views import RevisionMixin
class ItemList(generic.ListView):
@@ -35,7 +34,7 @@ class TraineeDetail(views.ProfileDetail):
if self.request.user.pk == self.object.pk:
context["page_title"] = "Your Training Record"
else:
context["page_title"] = f"{self.object.full_name}'s Training Record"
context["page_title"] = "{}'s Training Record".format(self.object.first_name + " " + self.object.last_name)
context["started_levels"] = self.object.started_levels()
context["completed_levels"] = self.object.level_qualifications.all()
context["categories"] = models.TrainingCategory.objects.all().prefetch_related('items')
@@ -64,7 +63,7 @@ class TraineeItemDetail(generic.ListView):
context = super().get_context_data(**kwargs)
trainee = models.Trainee.objects.get(pk=self.kwargs['pk'])
context["trainee"] = models.Trainee.objects.get(pk=self.kwargs['pk'])
context["page_title"] = f"Detailed Training Record for <a href='{trainee.get_absolute_url()}'>{trainee}</a>"
context["page_title"] = "Detailed Training Record for <a href='{}'>{}</a>".format(trainee.get_absolute_url(), trainee)
return context
@@ -74,7 +73,7 @@ class LevelDetail(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"Training Level {self.object} {self.object.get_icon}"
context["page_title"] = "Training Level {} {}".format(self.object, self.object.get_icon)
context["users_with"] = map(lambda qual: qual.trainee, models.TrainingLevelQualification.objects.filter(level=self.object))
context["u"] = models.Trainee.objects.get(pk=self.kwargs['u']) if 'u' in self.kwargs else self.request.user
return context
@@ -108,9 +107,6 @@ class TraineeList(generic.ListView):
# not an integer
pass
if self.request.GET.get('is_supervisor', ''):
filt = filt & Q(is_supervisor=True)
return self.model.objects.filter(filt).annotate(num_qualifications=Count('qualifications_obtained')).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item')
def get_context_data(self, **kwargs):
@@ -124,6 +120,8 @@ class AddQualification(generic.CreateView, ModalURLMixin):
model = models.TrainingItemQualification
form_class = forms.QualificationForm
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['trainee'])
return super().form_valid(form, *args, **kwargs)
@@ -135,15 +133,14 @@ class AddQualification(generic.CreateView, ModalURLMixin):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_training.html'
trainee = models.Trainee.objects.get(pk=self.kwargs['pk'])
context['page_title'] = f"Add Qualification for {trainee}"
context['page_title'] = "Add Qualification for {}".format(models.Trainee.objects.get(pk=self.kwargs['pk']))
return context
def get_success_url(self):
return self.get_close_url('add_qualification', 'trainee_detail')
return self.get_close_url('trainee_detail', 'trainee_detail')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs = super(AddQualification, self).get_form_kwargs()
kwargs['pk'] = self.kwargs['pk']
return kwargs
@@ -156,8 +153,7 @@ class EditQualification(generic.UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["depths"] = models.TrainingItemQualification.CHOICES
trainee = models.Trainee.objects.get(pk=self.kwargs['pk'])
context['page_title'] = f"Edit Qualification {self.object} for {trainee}"
context['page_title'] = "Edit Qualification {} for {}".format(self.object, models.Trainee.objects.get(pk=self.kwargs['pk']))
return context
def get_form_kwargs(self):
@@ -179,8 +175,7 @@ class AddLevelRequirement(generic.CreateView, ModalURLMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
level = models.TrainingLevel.objects.get(pk=self.kwargs['pk'])
context["page_title"] = f"Add Requirements to Training Level {level}"
context["page_title"] = "Add Requirements to Training Level {}".format(models.TrainingLevel.objects.get(pk=self.kwargs['pk']))
return context
def get_form_kwargs(self):
@@ -225,11 +220,9 @@ class ConfirmLevel(generic.RedirectView):
level_qualification, created = models.TrainingLevelQualification.objects.get_or_create(trainee=trainee, level=models.TrainingLevel.objects.get(pk=kwargs['level_pk']))
if created:
user = self.request.user
reversion.set_user(user)
level_qualification.confirmed_by = self.request.user
level_qualification.confirmed_on = timezone.now()
level_qualification.save()
reversion.add_to_revision(trainee)
reversion.add_to_revision(trainee)
return reverse_lazy('trainee_detail', kwargs={'pk': kwargs['pk']})

View File

@@ -14,8 +14,8 @@
{% for version in object_list %}
<tr>
<th scope="row">{{ version.revision.date_created }}</th>
<td><a href="{{ version.changes.new.get_absolute_url }}">{{ version.display_name }}</a></td>
<td>{{ version.display_id }}</td>
<td><a href="{{ version.changes.new.get_absolute_url }}">{{ version.changes.new.display_id|default:version.changes.new.pk }} | {{version.changes.new|to_class_name}}</a></td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{% include 'partials/linked_name.html' with profile=version.revision.user %}</td>
<td>
{% if version.changes.old == None %}

View File

@@ -13,7 +13,7 @@
{% for version in object_list %}
<tr>
<th scope="row">{{ version.revision.date_created }}</th>
<td>{{ version.display_id }}</td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.changes.old is None %}

View File

@@ -9,12 +9,12 @@ from reversion.models import Version, VersionQuerySet
class RevisionMixin:
@property
def is_first_version(self):
versions = RIGSVersion.objects.get_for_object(self)
versions = Version.objects.get_for_object(self)
return len(versions) == 1
@property
def current_version(self):
version = RIGSVersion.objects.get_for_object(self).select_related('revision').first()
version = Version.objects.get_for_object(self).select_related('revision').first()
return version
@property
@@ -36,7 +36,7 @@ class RevisionMixin:
version = self.current_version
if version is None:
return None
return version.display_id
return "V{0} | R{1}".format(version.pk, version.revision.pk)
@property
def date_created(self):
@@ -148,11 +148,9 @@ class ModelComparison:
@cached_property
def item_changes(self):
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
if self.follow and self.version.object is not None:
item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type)
new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(EventAuthorisation))
comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'date_joined']}
@@ -236,16 +234,3 @@ class RIGSVersion(Version):
old=self.parent._object_version.object if self.parent else None,
follow=True
)
@property
def display_id(self):
return f"V{self.pk} | R{self.revision.pk}"
@property
def display_name(self):
if hasattr(self.changes.new, 'display_id'):
id = self.changes.new.display_id
else:
id = self.changes.new.pk
return f"{id} | {self.changes.new.__class__.__name__}"