Compare commits

..

56 Commits

Author SHA1 Message Date
4416e5bfcb Add RevisionMixin in the right places 2021-10-20 21:15:59 +01:00
21276bcca0 You may not confirm your own training 2021-10-20 21:06:59 +01:00
bc465d67e9 Convert requirement addition to a modal 2021-10-20 21:02:19 +01:00
a644735cd6 Merge branch 'master' into training 2021-10-20 20:22:59 +01:00
10326f884f Fix the modal fuckery 2021-10-20 20:15:13 +01:00
0a0c9f15af Common competencies also do not count for being a supervisor 2021-10-09 10:38:40 +01:00
dbb9e3e530 Add 'is van driver' to trainee list, haulage super doesn't count as a supervisor 2021-10-09 10:29:03 +01:00
55558d1a4a Merge branch 'master' into training
# Conflicts:
#	RIGS/templates/risk_assessment_form.html
#	templates/base.html
2021-10-08 18:40:39 +01:00
081c33ebc8 Various tweaks 2021-09-13 01:07:17 +01:00
75410db752 Refactor is_supervisor 2021-09-13 00:49:02 +01:00
06c6b9a36e Change homepage links to match header ones 2021-09-12 20:26:05 +01:00
13b1cea28b Fancy training level list layout 2021-09-12 18:08:13 +01:00
cddb76bf7e Order training items by number 2021-09-08 20:44:02 +01:00
f4f1fb66a2 Leaderboard of qualifications obtained 2021-09-08 20:44:02 +01:00
d80aeca01f Add training level list
Plus various other fettling
2021-09-03 22:34:25 +01:00
45dfe2db51 Work on trainee reversion 2021-09-02 10:23:53 +01:00
de5997b9da Reversion working for training level 2021-08-29 22:19:30 +01:00
4a121964dc Start training navbar 2021-08-21 11:42:31 +01:00
df5e4c8e0a Level detail tweaking 2021-08-21 01:44:26 +01:00
3601c14ab7 Oops 2021-08-21 01:36:35 +01:00
adde6496f5 Add loads more sample training items
Hopefully the generator won't make levels with no requirements anymore now
2021-08-21 00:45:35 +01:00
ad734d94b2 Display pre pre requisite levels on level detail 2021-08-21 00:32:04 +01:00
7d3ada822d Common competencies sample data 2021-08-21 00:28:24 +01:00
732af53fda Groundwork stuff for common competencies + other fixes 2021-08-21 00:09:00 +01:00
4fb0529cc0 Modalify the training record addition form 2021-08-20 21:52:33 +01:00
aa23b1cd09 Add sharepoint link to new homepage
Good at scope, me
2021-08-20 21:52:17 +01:00
0c4228da57 Add a view for a trainee's item record 2021-08-20 14:26:32 +01:00
246a52d19e Don't try and create existing level qualifications 2021-08-20 13:48:30 +01:00
8b10aaf700 Display users with level on level detail page 2021-08-20 12:48:54 +01:00
4d0d4f02aa Generate a sample supervisor 2021-08-20 12:38:30 +01:00
af987c1ebb Make TrainingLevelRequirement the correct level of unique
Also updates generateSampleData to match
2021-08-19 18:47:38 +01:00
d406a911bb Initial refactoring of profile detail 2021-08-19 18:27:52 +01:00
63c5a68933 Goddamnit 2021-08-19 18:12:15 +01:00
66f7f830db Forgot that needed migrations generating 2021-08-19 18:08:57 +01:00
9590c2066d Validate that only supervisors may be supervisors 2021-08-19 16:19:46 +01:00
8b48b02ca7 Force trainingitemqualifications to be unique 2021-08-19 16:00:31 +01:00
68e7ec2a0d Merge branch 'master' into training 2021-08-19 15:49:16 +01:00
5779ebdf7e Merge branch 'master' into training
# Conflicts:
#	templates/base.html
2021-08-17 21:35:10 +01:00
be648c20d5 Level confirmation works 2021-07-29 23:41:35 +01:00
b6ef7c1d89 Force traininglevelqualifications to be unique 2021-07-29 23:15:09 +01:00
85f40b358a Some attempts at optimising SQL queries
New high score!
2021-07-29 22:49:27 +01:00
2698798035 Percentage complete works
Ain't half slow though!
2021-07-16 04:05:55 +01:00
dbaab5cf8c Autofire of traininglevelqualification basically works 2021-07-16 02:58:42 +01:00
0a9f82e480 Fettling with level granting logic
Untested as all of my forms broke I guess
2021-07-07 17:38:44 +01:00
54f2bd36bd UI for editing training level requirements 2021-07-06 22:10:15 +01:00
e836195fef mild polishing 2021-07-06 14:51:43 +01:00
68a424d62b Some sample data and UX work 2021-07-06 12:16:43 +01:00
5e15b8bb59 Basic UX for adding requirements to training levels 2021-07-06 11:37:04 +01:00
d26c1b535e item ui vaguely working 2021-07-06 00:09:46 +01:00
dff5ac2308 Whee broken HEAD 2021-07-05 23:24:13 +01:00
a3729fa930 Session log form work 2021-07-05 18:24:24 +01:00
458a734331 Machine switch 2021-07-01 09:50:13 +01:00
b1646d556c Start work on sample data command 2021-06-30 15:56:28 +01:00
f8624d3b7a Restructure based on actual thought put in by @mattysmith22 2021-06-30 15:17:00 +01:00
f6836fdab6 Merge branch 'master' into training 2021-06-29 17:17:48 +01:00
b3949f2903 Initial sketching 2021-06-29 17:13:36 +01:00
108 changed files with 5491 additions and 6244 deletions

1
.gitignore vendored
View File

@@ -26,7 +26,6 @@ var/
.installed.cfg
*.egg
node_modules/
data/
# Continer extras
.vagrant

10
Pipfile
View File

@@ -19,7 +19,7 @@ cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.2"
Django = "~=3.1.12"
django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
@@ -33,11 +33,12 @@ envparse = "~=0.2.0"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
lxml = "~=4.7.1"
importlib-metadata = "~=3.4.0"
lxml = "~=4.6.3"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.0.0"
Pillow = "~=8.3.2"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
@@ -77,8 +78,6 @@ sentry-sdk = "*"
diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
[dev-packages]
selenium = "~=3.141.0"
@@ -90,7 +89,6 @@ pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.9"

872
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -188,8 +188,12 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7
# CAPTCHA settings
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
if DEBUG or CI:
HCAPTCHA_SITEKEY = '10000000-ffff-ffff-ffff-000000000001'
HCAPTCHA_SECRET = '0x0000000000000000000000000000000000000000'
else:
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET')
# Email
EMAILER_TEST = False
@@ -260,5 +264,3 @@ USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View File

@@ -84,7 +84,7 @@ class BootstrapSelectElement(Region):
return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected):
options = [x for x in self.options if x.name == name]
options = list((x for x in self.options if x.name == name))
assert len(options) == 1
options[0].set_selected(selected)
@@ -117,15 +117,6 @@ class TextBox(Region):
self.root.send_keys(value)
class SimpleMDETextArea(Region):
@property
def value(self):
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.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
class CheckBox(Region):
def toggle(self):
self.root.click()

View File

@@ -8,13 +8,18 @@ from pytest_django.asserts import assertRedirects, assertContains, assertNotCont
from pytest_django.asserts import assertTemplateUsed, assertInHTML
from PyRIGS import urls
from RIGS.models import Event, Profile
from RIGS.models import Event
from assets.models import Asset
from django.db import connection
import pytest
from django.core.management import call_command
from django.template.defaultfilters import striptags
from django.urls.exceptions import NoReverseMatch
from django.test import TestCase, TransactionTestCase
from RIGS.models import Event
from assets.models import Asset
from django.db import connection
from django.test import TestCase
from django.test.utils import override_settings
@@ -44,7 +49,7 @@ def get_request_url(url):
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users'])
'deleteSampleData'])
def test_production_exception(command):
from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"):
@@ -62,76 +67,79 @@ class TestSampleDataGenerator(TestCase):
assert Event.objects.all().count() == 0
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
call_command('generateSampleData')
for url in find_urls_recursive(urls.urlpatterns):
request_url = get_request_url(url)
if request_url and 'user' not in request_url: # User module is full of edge cases
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'Login')
if 'application/json+oembed' in response.content.decode():
assertTemplateUsed(response, 'login_redirect.html')
else:
if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def setUp(self):
call_command('generateSampleData')
def test_unauthenticated(self): # Nothing should be available to the unauthenticated
for url in find_urls_recursive(urls.urlpatterns):
request_url = get_request_url(url)
if request_url and 'user' not in request_url: # User module is full of edge cases
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'Login')
if 'application/json+oembed' in response.content.decode():
assertTemplateUsed(response, 'login_redirect.html')
else:
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
call_command('deleteSampleData')
if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else:
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
def test_page_titles(self):
assert self.client.login(username='superuser', password='superuser')
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url)
response = self.client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = striptags(response.context_data["page_title"])
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
self.client.logout()
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_basic_access(client):
call_command('generateSampleData')
assert client.login(username="basic", password="basic")
def test_basic_access(self):
assert self.client.login(username="basic", password="basic")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons NOT shown in list
assertNotContains(response, 'Edit')
assertNotContains(response,
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons NOT shown in list
assertNotContains(response, 'Edit')
assertNotContains(response,
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(url)
assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History')
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urlz:
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(request_url, follow=True)
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urlz:
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = self.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 = self.client.get(request_url, follow=True)
assert response.status_code == 403
self.client.logout()
request_url = reverse('supplier_update', kwargs={'pk': 1})
response = client.get(request_url, follow=True)
assert response.status_code == 403
client.logout()
call_command('deleteSampleData')
def test_keyholder_access(self):
assert self.client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_keyholder_access(client):
call_command('generateSampleData')
assert client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
client.logout()
call_command('deleteSampleData')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
self.client.logout()

View File

@@ -50,7 +50,7 @@ class SecureAPIRequest(generic.View):
'profile': 'RIGS.view_profile',
'event': None,
'supplier': None,
'training_item': None, # TODO
'training_item': None, # TODO
}
'''
@@ -78,9 +78,6 @@ class SecureAPIRequest(generic.View):
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
filters = request.GET.get('filters', [])
if filters:
filters = filters.split(",")
# Supply data for one record
if pk:
@@ -101,13 +98,8 @@ class SecureAPIRequest(generic.View):
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
queries.append(reduce(operator.or_, qs))
for f in filters:
q = Q(**{f: True})
queries.append(q)
# Build the data response list
results = []
query = reduce(operator.and_, queries)

View File

@@ -0,0 +1 @@
default_app_config = 'RIGS.apps.RIGSAppConfig'

View File

@@ -24,7 +24,7 @@ class InvoiceIndex(generic.ListView):
template_name = 'invoice_list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(InvoiceIndex, self).get_context_data(**kwargs)
total = 0
for i in context['object_list']:
total += i.balance
@@ -41,9 +41,8 @@ class InvoiceDetail(generic.DetailView):
template_name = 'invoice_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
context = super(InvoiceDetail, self).get_context_data(**kwargs)
context['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed:
@@ -118,7 +117,7 @@ class InvoiceArchive(generic.ListView):
paginate_by = 25
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(InvoiceArchive, self).get_context_data(**kwargs)
context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context
@@ -197,7 +196,7 @@ class PaymentCreate(generic.CreateView):
template_name = 'payment_form.html'
def get_initial(self):
initial = super().get_initial()
initial = super(generic.CreateView, self).get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk is None:
raise Http404()

View File

@@ -8,7 +8,6 @@ from django.utils import timezone
from reversion import revisions as reversion
from RIGS import models
from training.models import TrainingLevel
# Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
@@ -97,10 +96,10 @@ class EventForm(forms.ModelForm):
raise forms.ValidationError(
'You haven\'t provided any client contact details. Please add a person or organisation.',
code='contact')
return super().clean()
return super(EventForm, self).clean()
def save(self, commit=True):
m = super().save(commit=False)
m = super(EventForm, self).save(commit=False)
if (commit):
m.save()
@@ -139,7 +138,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
self.fields['uni_id'].required = True
self.fields['account_code'].required = True
@@ -154,7 +153,7 @@ class EventAuthorisationRequestForm(forms.Form):
class EventRiskAssessmentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
super(EventRiskAssessmentForm, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
if str(name) == 'supervisor_consulted':
field.widget = forms.CheckboxInput()
@@ -165,9 +164,6 @@ class EventRiskAssessmentForm(forms.ModelForm):
], attrs={'class': 'custom-control-input', 'required': 'true'})
def clean(self):
if self.cleaned_data.get('big_power'):
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
# Check expected values
unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items():
@@ -185,7 +181,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
class EventChecklistForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
super(EventChecklistForm, self).__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:

View File

@@ -5,7 +5,6 @@ from assets import models
from RIGS import models as rigsmodels
from training import models as tmodels
class Command(BaseCommand):
help = 'Deletes testing sample data'
@@ -35,8 +34,6 @@ class Command(BaseCommand):
self.delete_objects(tmodels.TrainingCategory)
self.delete_objects(tmodels.TrainingItem)
self.delete_objects(tmodels.TrainingLevel)
self.delete_objects(tmodels.TrainingItemQualification)
self.delete_objects(tmodels.TrainingLevelRequirement)
def delete_objects(self, model):
for obj in model.objects.all():

View File

@@ -3,7 +3,6 @@
from django.db import models, migrations
import RIGS.models
import versioning
class Migration(migrations.Migration):
@@ -26,6 +25,6 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
]

View File

@@ -3,7 +3,6 @@
from django.db import models, migrations
import RIGS.models
import versioning
class Migration(migrations.Migration):
@@ -22,6 +21,6 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
]

View File

@@ -4,7 +4,6 @@
from django.db import models, migrations
from django.conf import settings
import RIGS.models
import versioning
class Migration(migrations.Migration):
@@ -42,7 +41,7 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='EventItem',
@@ -71,7 +70,7 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.AddField(
model_name='event',

View File

@@ -4,7 +4,6 @@ import RIGS.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import versioning
class Migration(migrations.Migration):
@@ -59,7 +58,7 @@ class Migration(migrations.Migration):
'ordering': ['event'],
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistCrew',
@@ -70,7 +69,7 @@ class Migration(migrations.Migration):
('end', models.DateTimeField()),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
],
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistVehicle',
@@ -79,7 +78,7 @@ class Migration(migrations.Migration):
('vehicle', models.CharField(max_length=255)),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
],
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='RiskAssessment',
@@ -118,7 +117,7 @@ class Migration(migrations.Migration):
'ordering': ['event'],
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.RemoveField(
model_name='eventcrew',

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.1.13 on 2021-10-27 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0042_auto_20211007_2338'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='initials',
field=models.CharField(max_length=5, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.11 on 2022-01-09 14:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0043_auto_20211027_1519'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_supervisor',
field=models.BooleanField(default=False),
),
]

View File

@@ -19,17 +19,15 @@ from reversion import revisions as reversion
from reversion.models import Version
@reversion.register
class Profile(AbstractUser):
initials = models.CharField(max_length=5, null=True, blank=False)
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False)
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False)
is_supervisor = models.BooleanField(default=False)
reversion_hide = True
@classmethod
def make_api_key(cls):
@@ -68,8 +66,10 @@ class Profile(AbstractUser):
def __str__(self):
return self.name
# TODO move to versioning - currently get import errors with that
class RevisionMixin:
class RevisionMixin(object):
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
@@ -99,7 +99,7 @@ class RevisionMixin:
version = self.current_version
if version is None:
return None
return f"V{version.pk} | R{version.revision.pk}"
return "V{0} | R{1}".format(version.pk, version.revision.pk)
class Person(models.Model, RevisionMixin):
@@ -207,7 +207,7 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at'
def __str__(self):
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
class Venue(models.Model, RevisionMixin):
@@ -344,14 +344,11 @@ class Event(models.Model, RevisionMixin):
@property
def display_id(self):
if self.pk:
if self.is_rig:
return str("N%05d" % self.pk)
if self.is_rig:
return str("N%05d" % self.pk)
else:
return self.pk
return "????"
# Calculated values
"""
EX Vat
@@ -475,7 +472,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.display_id}: {self.name}"
return "{}: {}".format(self.display_id, self.name)
def clean(self):
errdict = {}
@@ -521,11 +518,11 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order']
def __str__(self):
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
@property
def activity_feed_string(self):
return f"item {self.name}"
return str("item {}".format(self.name))
@reversion.register
@@ -543,7 +540,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
@property
def activity_feed_string(self):
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
class InvoiceManager(models.Manager):
@@ -671,6 +668,7 @@ class RiskAssessment(models.Model, RevisionMixin):
# Power
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
# If yes to the above two, you must answer...
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
outside = models.BooleanField(help_text="Is the event outdoors?")

View File

@@ -38,7 +38,7 @@ class RigboardIndex(generic.TemplateView):
def get_context_data(self, **kwargs):
# get super context
context = super().get_context_data(**kwargs)
context = super(RigboardIndex, self).get_context_data(**kwargs)
# call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
@@ -50,7 +50,7 @@ class WebCalendar(generic.TemplateView):
template_name = 'calendar.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(WebCalendar, self).get_context_data(**kwargs)
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
return context
@@ -61,8 +61,8 @@ class EventDetail(generic.DetailView):
model = models.Event
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
title = f"{self.object.display_id} | {self.object.name}"
context = super(EventDetail, self).get_context_data(**kwargs)
title = "{} | {}".format(self.object.display_id, self.object.name)
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title
@@ -84,7 +84,7 @@ class EventCreate(generic.CreateView):
template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(EventCreate, self).get_context_data(**kwargs)
context['page_title'] = "New Event"
context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate()
@@ -110,8 +110,8 @@ class EventUpdate(generic.UpdateView):
template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Event {self.object.display_id}"
context = super(EventUpdate, self).get_context_data(**kwargs)
context['page_title'] = "Event {}".format(self.object.display_id)
context['edit'] = True
form = context['form']
@@ -134,7 +134,7 @@ class EventUpdate(generic.UpdateView):
if hasattr(self.object, 'authorised'):
messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
return super().render_to_response(context, **response_kwargs)
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
@@ -142,7 +142,7 @@ class EventUpdate(generic.UpdateView):
class EventDuplicate(EventUpdate):
def get_object(self, queryset=None):
old = super().get_object(queryset) # Get the object (the event you're duplicating)
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO
@@ -151,7 +151,6 @@ class EventDuplicate(EventUpdate):
# Clear checked in by if it's a dry hire
if new.dry_hire is True:
new.checked_in_by = None
new.collector = None
# Remove all the authorisation information from the new event
new.auth_request_to = ''
@@ -167,8 +166,8 @@ class EventDuplicate(EventUpdate):
return new
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context = super(EventDuplicate, self).get_context_data(**kwargs)
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
context["duplicate"] = True
return context
@@ -210,7 +209,8 @@ class EventArchive(generic.ListView):
paginate_by = 25
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# get super context
context = super(EventArchive, self).get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
@@ -265,7 +265,7 @@ class EventArchive(generic.ListView):
# Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic')
if not qs.exists():
if len(qs) == 0:
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs
@@ -282,7 +282,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' +
f'You will also receive email confirmation to {self.object.email}.')
'You will also receive email confirmation to %s.' % self.object.email)
return self.render_to_response(self.get_context_data())
@property
@@ -296,10 +296,10 @@ class EventAuthorise(generic.UpdateView):
return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(EventAuthorise, self).get_context_data(**kwargs)
context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
@@ -318,7 +318,7 @@ class EventAuthorise(generic.UpdateView):
return super(EventAuthorise, self).get(request, *args, **kwargs)
def get_form(self, **kwargs):
form = super().get_form(**kwargs)
form = super(EventAuthorise, self).get_form(**kwargs)
form.instance.event = self.event
form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by
@@ -334,7 +334,7 @@ class EventAuthorise(generic.UpdateView):
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL")
return super().dispatch(request, *args, **kwargs)
return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
@@ -344,7 +344,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
@method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
@property
def object(self):
@@ -405,13 +405,13 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css')
response = super().render_to_response(context, **response_kwargs)
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
context['hmac'] = signing.dumps({
'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

After

Width:  |  Height:  |  Size: 852 KiB

File diff suppressed because one or more lines are too long

View File

@@ -114,7 +114,7 @@
<div class="card mb-3">
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
<div class="card-body">
{% if object.event.riskassessment.event_size == 0 %}
{% if event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">

View File

@@ -1,7 +1,5 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% block content %}
<div class="row my-3 py-3">
{% if not request.is_ajax %}
@@ -45,7 +43,7 @@
{% if perms.RIGS.view_event %}
<h4>Notes</h4>
<hr>
<p class="dont-break-out">{{ event.notes|markdown }}</p>
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p>
{% endif %}
<br>
{% include 'partials/item_table.html' %}

View File

@@ -8,13 +8,11 @@
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.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/simplemde.min.js' %}"></script>
{% endblock %}
{% block js %}
@@ -65,21 +63,11 @@
{% endif %}
});
$(document).ready(function () {
setupMDE('#id_description');
setupMDE('#id_notes');
setupMDE('#item_description');
$('#itemModal').on('shown.bs.modal', function (e) {
$('#item_description').data('mde_editor').value(
$('#item_description').val()
);
});
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
});
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
})
</script>
{% endblock %}
@@ -180,7 +168,7 @@
<label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
<div class="col-sm-12">
<div class="col-sm-8">
{% render_field form.description class+="form-control" %}
</div>
</div>
@@ -357,7 +345,7 @@
<div class="col-sm-12">
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
{% render_field form.notes class+="form-control md-enabled" %}
{% render_field form.notes class+="form-control" %}
</div>
</div>
{% include 'partials/item_table.html' %}

View File

@@ -74,14 +74,6 @@
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle>
<listStyle name="ol"
bulletFormat="%s."
bulletFontSize="10" />
<listStyle name="ul"
start="bulletchar"
bulletFontSize="10"/>
</stylesheet>
<template > {# Note: page is 595x842 points (1 point=1/72in) #}

View File

@@ -1,6 +1,4 @@
{% load markdown_tags %}
{% load filters %}
<setNextFrame name="main"/>
<nextFrame/>
<blockTable style="headLayout" colWidths="330,165">
@@ -12,8 +10,10 @@
<b>{{object.start_date|date:"D jS N Y"}}</b>
</para>
<keepInFrame maxHeight="500" onOverflow="shrink">
{{ object.description|default_if_none:""|markdown:"rml" }}
<keepInFrame>
<para style="style.event_description">
{{ object.description|default_if_none:""|linebreaksxml }}
</para>
</keepInFrame>
</td>
<td>
@@ -184,7 +184,7 @@
{% if item.description %}
</para>
<para style="item_description">
{{ item.description|markdown:"rml" }}
<em>{{ item.description|linebreaksxml }}</em>
</para>
<para>
{% endif %}

View File

@@ -1,48 +1,24 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load button from filters %}
{% block title %}Request Authorisation{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="alert alert-warning pb-0">
<div class="alert alert-warning">
<h1>Send authorisation request email.</h1>
<p>Pressing send will email the address provided. <strong>Please triple check everything before continuing.</strong></p>
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p>
</div>
<div class="alert alert-info pb-0">
{% if object.person.email or object.organisation.email %}
<div class="alert alert-info">
<dl class="dl-horizontal">
{% if object.person.email %}
<dt>Person Email</dt>
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
{% endif %}
{% if object.organisation.email %}
<dd>{{ object.person.email }}</dd>
<dt>Organisation Email</dt>
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
{% endif %}
<dd>{{ object.organisation.email }}</dd>
</dl>
{% else %}
<p>No email addresses saved to the client &#3232;_&#3232;</p>
{% endif %}
</div>
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
{% csrf_token %}

View File

@@ -1,9 +1,8 @@
{% load markdown_tags %}
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
<th scope="row">
<span class="name">{{ item.name }}</span>
<div class="item-description">
<em class="description">{{item.description|markdown}}</em>
<em class="description">{{item.description|linebreaksbr}}</em>
</div>
</th>
{% if perms.RIGS.view_event %}

View File

@@ -46,12 +46,15 @@
<div class="card-header">Event Info</div>
<div class="card-body">
<dl class="row">
{% if event.venue %}
<dt class="col-sm-5">Event Venue</dt>
<dd class="col-sm-7">
{{ event.venue }}
{% if object.venue %}
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
{{ object.venue }}
</a>
{% endif %}
</dd>
{% endif %}
<dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">{{ event.get_status_display }}</dd>

View File

@@ -1,5 +1,4 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="card card-info">
<div class="card-header">Event Info</div>
<div class="card-body">
@@ -47,7 +46,7 @@
<dd class="col-sm-12">&nbsp;</dd>
<dt class="col-sm-6">Event Description</dt>
<dd class="dont-break-out col-sm-12">{{ event.description|markdown }}</dd>
<dd class="dont-break-out col-sm-12">{{ event.description|linebreaksbr }}</dd>
<dd class="col-sm-12">&nbsp;</dd>

View File

@@ -16,10 +16,10 @@
id="item_name"/>
</div>
</div>
<div class="form-group form-row" data-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
<div class="form-group form-row">
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<textarea type="text" placeholder="Description" class="form-control md-enabled"
<textarea type="text" placeholder="Description" class="form-control"
id="item_description" rows="8"></textarea>
</div>
</div>

View File

@@ -23,7 +23,7 @@
</thead>
<tbody id="item-table-body">
{% for item in object.items.all %}
{% include 'partials/item_row.html' %}
{% include 'item_row.html' %}
{% endfor %}
</tbody>
{% if auth or perms.RIGS.view_event %}

View File

@@ -114,8 +114,10 @@ def orderby(request, field, attr):
return dict_.urlencode()
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True)
def get_field(obj, field, autoescape=True):
value = getattr(obj, field)
if(isinstance(value, bool)):
@@ -210,8 +212,6 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
clazz += " btn-primary "
icon = "fa-plus"
text = "New"
elif type == 'copy':
return {'copy': True, 'id': id, 'style': style}
elif type == 'search':
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
elif type == 'submit':
@@ -219,7 +219,7 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
@register.simple_tag # TODO Can these be done with annotation/aggregation?
@register.simple_tag
def invoices_waiting():
return len(models.Event.objects.waiting_invoices())

View File

@@ -1,56 +0,0 @@
from bs4 import BeautifulSoup
from django import template
from django.utils.safestring import mark_safe
import markdown
__author__ = 'ghost'
register = template.Library()
@register.filter(name="markdown")
def markdown_filter(text, input_format='html'):
# markdown library can't handle text=None
if text is None:
return text
html = markdown.markdown(text, extensions=['markdown.extensions.nl2br'])
# Convert format to RML
soup = BeautifulSoup(html, "html.parser")
# Prevent code injection
for script in soup('script'):
script.string = "Your script shall not pass!"
if input_format == 'html':
return mark_safe('<div class="markdown">' + str(soup) + '</div>')
elif input_format == 'rml':
# Image aren't supported so remove them
for img in soup('img'):
img.parent.extract()
# <code> should become <font>
for c in soup('code'):
c.name = 'font'
c['face'] = "Courier"
# blockquotes don't exist but we can still do something to show
for bq in soup('blockquote'):
bq.name = 'pre'
bq.string = bq.text
for alist in soup.find_all(['ul', 'ol']):
alist['style'] = alist.name
for li in alist.find_all('li', recursive=False):
text = li.find(text=True)
text.wrap(soup.new_tag('p'))
if alist.parent.name != 'li':
indent = soup.new_tag('indent')
indent['left'] = '0.6cm'
alist.wrap(indent)
# Paragraphs have a different tag
for p in soup('p'):
p.name = 'para'
return mark_safe(str(soup))

View File

@@ -96,7 +96,7 @@ class CreateEvent(FormPage):
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
form_items = {
'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')),
'description': (regions.TextBox, (By.ID, 'id_description')),
'name': (regions.TextBox, (By.ID, 'id_name')),
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
@@ -110,7 +110,7 @@ class CreateEvent(FormPage):
'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
'po': (regions.TextBox, (By.ID, 'id_purchase_order')),
'notes': (regions.SimpleMDETextArea, (By.ID, 'id_notes'))
'notes': (regions.TextBox, (By.ID, 'id_notes'))
}
def select_event_type(self, type_name):

View File

@@ -1,7 +1,7 @@
from pypom import Region
from selenium.webdriver.common.by import By
from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
from PyRIGS.tests.regions import TextBox, Modal
class Header(Region):
@@ -42,7 +42,7 @@ class ItemModal(Modal):
form_items = {
'name': (TextBox, (By.ID, 'item_name')),
'description': (SimpleMDETextArea, (By.ID, 'item_description')),
'description': (TextBox, (By.ID, 'item_description')),
'quantity': (TextBox, (By.ID, 'item_quantity')),
'price': (TextBox, (By.ID, 'item_cost'))
}

View File

@@ -3,8 +3,6 @@ from datetime import date
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.safestring import SafeText
from RIGS.templatetags.markdown_tags import markdown_filter
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
@@ -172,7 +170,6 @@ class TestInvoiceDelete(TestCase):
def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True)
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
2: models.Event.objects.create(name="TE E2", start_date=date.today())
@@ -284,11 +281,11 @@ def test_xframe_headers(admin_client, basic_event):
response = admin_client.get(event_url, follow=True)
with pytest.raises(KeyError):
response.headers["X-Frame-Options"]
response._headers["X-Frame-Options"]
response = admin_client.get(login_url, follow=True)
with pytest.raises(KeyError):
response.headers["X-Frame-Options"]
response._headers["X-Frame-Options"]
def test_oembed(client, basic_event):
@@ -366,215 +363,6 @@ def test_checklist_review(admin_client, admin_user, checklist):
def test_ra_redirect(admin_client, admin_user, ra):
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
response = admin_client.get(request_url, follow=True)
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
class TestMarkdownTemplateTags(TestCase):
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)
self.assertIsInstance(html, SafeText)
def test_img_strip(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertNotIn("<img", rml)
def test_code(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn('<font face="Courier">monospace</font>', rml)
def test_blockquote(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn("<pre>\nBlock quotes", rml)
def test_lists(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn("<li><para>second item</para></li>", rml) # <ol>
self.assertIn("<li><para>that one</para></li>", rml) # <ul>
def test_in_print(self):
event = models.Event.objects.create(
name="MD Print Test",
description=self.markdown,
start_date='2016-01-01',
)
user = models.Profile.objects.create(
username='RML test',
is_superuser=True, # Don't care about permissions
is_active=True,
)
user.set_password('rmltester')
user.save()
self.assertTrue(self.client.login(username=user.username, password='rmltester'))
response = self.client.get(reverse('event_print', kwargs={'pk': event.pk}))
self.assertEqual(response.status_code, 200)
# By the time we have a PDF it should be larger than the original by some margin
# RML hard fails if something doesn't work
self.assertGreater(len(response.content), len(self.markdown))
def test_nonetype(self):
html = markdown_filter(None)
self.assertIsNone(html)
def test_linebreaks(self):
html = markdown_filter(self.markdown)
self.assertIn("Itemized lists<br/>\nlook like", html)

View File

@@ -6,7 +6,7 @@ class PersonList(GenericListView):
model = models.Person
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(PersonList, self).get_context_data(**kwargs)
context['page_title'] = "People"
context['create'] = 'person_create'
context['edit'] = 'person_update'
@@ -19,7 +19,7 @@ class PersonDetail(GenericDetailView):
model = models.Person
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(PersonDetail, self).get_context_data(**kwargs)
context['history_link'] = 'person_history'
context['detail_link'] = 'person_detail'
context['update_link'] = 'person_update'
@@ -49,7 +49,7 @@ class OrganisationList(GenericListView):
model = models.Organisation
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(OrganisationList, self).get_context_data(**kwargs)
context['create'] = 'organisation_create'
context['edit'] = 'organisation_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation')
@@ -62,7 +62,7 @@ class OrganisationDetail(GenericDetailView):
model = models.Organisation
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(OrganisationDetail, self).get_context_data(**kwargs)
context['history_link'] = 'organisation_history'
context['detail_link'] = 'organisation_detail'
context['update_link'] = 'organisation_update'
@@ -92,7 +92,7 @@ class VenueList(GenericListView):
model = models.Venue
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(VenueList, self).get_context_data(**kwargs)
context['create'] = 'venue_create'
context['edit'] = 'venue_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_venue')
@@ -104,7 +104,7 @@ class VenueDetail(GenericDetailView):
model = models.Venue
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(VenueDetail, self).get_context_data(**kwargs)
context['history_link'] = 'venue_history'
context['detail_link'] = 'venue_detail'
context['update_link'] = 'venue_update'

View File

@@ -0,0 +1 @@
default_app_config = 'assets.apps.AssetsAppConfig'

View File

@@ -1,6 +1,3 @@
import urllib.parse
class AssetIDConverter: # Forces lowercase to uppercase
regex = '[^/]+'
@@ -9,16 +6,3 @@ class AssetIDConverter: # Forces lowercase to uppercase
def to_url(self, value):
return str(value).upper()
class ListConverter:
regex = '[^/]+'
def to_python(self, value):
return value.split(',')
def to_url(self, value):
string = ""
for i in value:
string += "," + str(i)
return string[1:]

View File

@@ -32,8 +32,6 @@ class AssetSearchForm(forms.Form):
q = forms.CharField(required=False)
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
is_cable = forms.BooleanField(required=False)
date_acquired = forms.DateField(required=False)
class SupplierForm(forms.ModelForm):
@@ -46,3 +44,11 @@ class CableTypeForm(forms.ModelForm):
class Meta:
model = models.CableType
fields = '__all__'
def clean(self): # TODO Does unique_together work better than this?
form_data = self.cleaned_data
queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores']))
# Being identical to itself shouldn't count...
if queryset.exists() and self.instance.pk != queryset[0].pk:
raise forms.ValidationError("A cable type that exactly matches this one already exists, please use that instead.", code="notunique")
return form_data

View File

@@ -1,17 +0,0 @@
# Generated by Django 3.2.11 on 2022-01-12 19:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0021_auto_20210302_1204'),
]
operations = [
migrations.AlterUniqueTogether(
name='cabletype',
unique_together={('plug', 'socket', 'circuits', 'cores')},
),
]

View File

@@ -6,8 +6,7 @@ from django.urls import reverse
from reversion import revisions as reversion
from reversion.models import Version
from RIGS.models import Profile
from versioning.versioning import RevisionMixin
from RIGS.models import RevisionMixin, Profile
class AssetCategory(models.Model):
@@ -76,11 +75,10 @@ class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
unique_together = ['plug', 'socket', 'circuits', 'cores']
def __str__(self):
if self.plug and self.socket:
return f"{self.plug.description}{self.socket.description}"
return "%s%s" % (self.plug.description, self.socket.description)
else:
return "Unknown"
@@ -149,7 +147,7 @@ class Asset(models.Model, RevisionMixin):
]
def __str__(self):
return f"{self.asset_id} | {self.description}"
return "{} | {}".format(self.asset_id, self.description)
def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -12,7 +12,7 @@
});
$('#searchButton').click(function (e) {
e.preventDefault();
var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val());
var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val();
$.ajax({
url: url,
success: function(){

View File

@@ -5,14 +5,11 @@
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.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/simplemde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %}
{% block js %}
@@ -75,11 +72,6 @@
preserveSelected: false
});
</script>
<script>
$(document).ready(function () {
setupMDE('#id_comments');
});
</script>
{% endblock %}
{% block content %}

View File

@@ -1,7 +1,6 @@
{% extends 'base_assets.html' %}
{% load paginator from filters %}
{% load button from filters %}
{% load ids_from_objects from asset_tags %}
{% load widget_tweaks %}
{% load static %}
@@ -61,54 +60,27 @@
{% block content %}
<div class="row">
<div class="col px-0">
<form id="asset-search-form" method="GET">
<div class="form-row">
<div class="col">
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
</div>
</div>
</div>
<div class="form-row mt-2">
<div class="col">
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="category" class="sr-only">Category</label>
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
</div>
</div>
<div class="col">
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
</div>
<div class="col mt-2">
<div class="form-check form-check-inline">
{% render_field form.is_cable|add_class:'form-check-input' %}
<label class="form-check-label" for="is_cable">Only Cables?</label>
</div>
</div>
<div class="col-auto">
<div class="form-group d-flex flex-nowrap">
<label for="date_acquired" class="text-nowrap mt-auto">Date Acquired</label>
{% render_field form.date_acquired|add_class:'form-control mx-2' %}
</div>
</div>
<div class="col-auto mr-auto">
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
</div>
</div>
<form id="asset-search-form" method="GET" class="form-inline justify-content-end">
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
</div>
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="category" class="sr-only">Category</label>
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
</div>
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
</form>
</div>
</div>
<div class="row my-2">
<div class="col text-right px-0">
{% button 'new' 'asset_create' style="width: 6em" %}
{% if object_list %}
<a class="btn btn-primary" href="{% url 'generate_labels' object_list|ids_from_objects %}"><span class="fas fa-barcode"></span> Generate Labels</a>
{% endif %}
</div>
</div>
<div class="row my-2">

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE document SYSTEM "rml.dtd">
{% load multiply from filters %}
{% load index from asset_tags %}
<document filename="{{filename}}">
<template>
<pageTemplate id="main">
<pageGraphics>
</pageGraphics>
<frame id="first" x1="5" y1="-10" width="581" height="842"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="table">
<!-- show a grid: this also comes in handy for debugging your tables.-->
<lineStyle kind="GRID" colorName="black" thickness="1" start="0,0" stop="-1,-1" />
</blockTableStyle>
</stylesheet>
<story>
<blockTable style="table">
{% for i in images0 %}
<tr>
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images3|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
</tr>
{% endfor %}
</blockTable>
</story>
</document>

View File

@@ -12,7 +12,9 @@
{% button 'edit' url='asset_update' pk=object.asset_id %}
{% button 'duplicate' url='asset_duplicate' pk=object.asset_id %}
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a>
{% if object.is_cable %}
<a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a>
{% endif %}
</div>
{% endif %}
{% if create or edit or duplicate %}

View File

@@ -1,6 +1,4 @@
{% load widget_tweaks %}
{% load markdown_tags %}
<div class="card">
<div class="card-header">
Asset Details
@@ -40,14 +38,14 @@
<!---TODO: Lower default number of lines in comments box-->
<div class="form-group">
<label for="{{ form.comments.id_for_label }}">Comments</label>
{% render_field form.comments|add_class:'form-control md-enabled' %}
{% render_field form.comments|add_class:'form-control' %}
</div>
{% else %}
<dt>Asset ID</dt>
<dd>{{ object.asset_id }}</dd>
<dt>Description</dt>
<dd>{{ object.description }}</dd>
<dd style="overflow-wrap: break-word;">{{ object.description }}</dd>
<dt>Category</dt>
<dd>{{ object.category }}</dd>
@@ -59,7 +57,7 @@
<dd>{{ object.serial_number|default:'-' }}</dd>
<dt>Comments</dt>
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|markdown }}</dd>
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|linebreaksbr }}</dd>
{% endif %}
</div>
</div>

View File

@@ -8,26 +8,11 @@
{% if create or edit or duplicate %}
<div class="form-group" id="purchased-from-group">
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
<div class="row">
<div class="col">
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
{% if object.purchased_from %}
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
{% endif %}
</select>
</div>
<div class="col align-right">
<div class="btn-group">
<a href="{% url 'supplier_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.purchased_from.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.supplier.value %}href="{% url 'supplier_update' form.purchased_from.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.purchased_from.id_for_label }}-update" data-target="#{{ form.purchased_from.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
</div>
</div>
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
{% if object.purchased_from %}
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
{% endif %}
</select>
</div>
<div class="form-group">

View File

@@ -1,17 +0,0 @@
from django import template
from assets import models
register = template.Library()
@register.filter
def ids_from_objects(object_list):
id_list = []
for obj in object_list:
id_list.append(obj.asset_id)
return id_list
@register.filter
def index(indexable, i):
return indexable[i] if i < len(indexable) else None

View File

@@ -70,14 +70,14 @@ class AssetList(BasePage):
class AssetForm(FormPage):
_purchased_from_select_locator = (By.XPATH, '//div[@id="purchased-from-group"]/div/div/div')
_purchased_from_select_locator = (By.CSS_SELECTOR, 'div#purchased-from-group>div.bootstrap-select')
_parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select')
form_items = {
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
'comments': (regions.TextBox, (By.ID, 'id_comments')),
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),

View File

@@ -180,7 +180,7 @@ class TestAssetForm(AutoLoginTest):
def test_asset_edit(self):
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertIsNotNone(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly'))
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
new_description = "Big Shelf"
self.page.description = new_description
@@ -335,7 +335,7 @@ class TestAssetAudit(AutoLoginTest):
self.assertNotIn(self.asset.asset_id, self.page.assets)
def test_audit_list(self):
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
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, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))

View File

@@ -64,11 +64,11 @@ def test_x_frame_headers(client, django_user_model, test_asset):
response = client.get(asset_url, follow=True)
with pytest.raises(KeyError):
response.headers["X-Frame-Options"]
response._headers["X-Frame-Options"]
response = client.get(login_url, follow=True)
with pytest.raises(KeyError):
response.headers["X-Frame-Options"]
response._headers["X-Frame-Options"]
def test_oembed(client, test_asset):
@@ -105,6 +105,7 @@ def test_asset_edit(admin_client, test_asset):
def test_cable_edit(admin_client, test_cable):
url = reverse('asset_update', kwargs={'pk': test_cable.asset_id})
# TODO Why do I have to send is_cable=True here?
response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
# TODO Can't figure out how to select the 'none' option...

View File

@@ -7,7 +7,6 @@ from PyRIGS.views import OEmbedView
from . import views, converters
register_converter(converters.AssetIDConverter, 'asset')
register_converter(converters.ListConverter, 'list')
urlpatterns = [
path('', login_required(views.AssetList.as_view()), name='asset_index'),
@@ -20,7 +19,6 @@ urlpatterns = [
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),

View File

@@ -1,8 +1,5 @@
import simplejson
import random
import base64
from io import BytesIO
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers
@@ -14,13 +11,10 @@ from django.utils.decorators import method_decorator
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from PyPDF2 import PdfFileMerger, PdfFileReader
from PIL import Image, ImageDraw, ImageFont
from barcode import Code39
from barcode.writer import ImageWriter
from z3c.rml import rml2pdf
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
is_ajax, OEmbedView
@@ -58,12 +52,6 @@ class AssetList(LoginRequiredMixin, generic.ListView):
else:
queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
if form.cleaned_data['is_cable']:
queryset = queryset.filter(is_cable=True)
if form.cleaned_data['date_acquired']:
queryset = queryset.filter(date_acquired=form.cleaned_data['date_acquired'])
if form.cleaned_data['category']:
queryset = queryset.filter(category__in=form.cleaned_data['category'])
@@ -76,7 +64,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
return queryset.select_related('category', 'status')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(AssetList, self).get_context_data(**kwargs)
context["form"] = self.form
if hasattr(self.form, 'cleaned_data'):
context["category_filters"] = self.form.cleaned_data.get('category')
@@ -117,7 +105,7 @@ class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"Asset {self.object.display_id}"
context["page_title"] = "Asset {}".format(self.object.display_id)
return context
@@ -130,7 +118,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
context = super().get_context_data(**kwargs)
context["edit"] = True
context["connectors"] = models.Connector.objects.all()
context["page_title"] = f"Edit Asset: {self.object.display_id}"
context["page_title"] = "Edit Asset: {}".format(self.object.display_id)
return context
def get_success_url(self):
@@ -150,7 +138,7 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
form_class = forms.AssetForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(AssetCreate, self).get_context_data(**kwargs)
context["create"] = True
context["connectors"] = models.Connector.objects.all()
context["page_title"] = "Create Asset"
@@ -177,9 +165,8 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
context = super().get_context_data(**kwargs)
context["create"] = None
context["duplicate"] = True
old_id = self.get_object().asset_id
context['previous_asset_id'] = old_id
context["page_title"] = f"Duplication of Asset: {old_id}"
context['previous_asset_id'] = self.get_object().asset_id
context["page_title"] = "Duplication of Asset: {}".format(context['previous_asset_id'])
return context
@@ -202,7 +189,7 @@ class AssetAuditList(AssetList):
return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(AssetAuditList, self).get_context_data(**kwargs)
context['page_title'] = "Asset Audit List"
return context
@@ -213,7 +200,7 @@ class AssetAudit(AssetEdit):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"Audit Asset: {self.object.display_id}"
context["page_title"] = "Audit Asset: {}".format(self.object.display_id)
return context
def get_success_url(self):
@@ -230,7 +217,7 @@ class SupplierList(GenericListView):
ordering = ['name']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(SupplierList, self).get_context_data(**kwargs)
context['create'] = 'supplier_create'
context['edit'] = 'supplier_update'
context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
@@ -257,7 +244,7 @@ class SupplierDetail(GenericDetailView):
model = models.Supplier
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(SupplierDetail, self).get_context_data(**kwargs)
context['history_link'] = 'supplier_history'
context['update_link'] = 'supplier_update'
context['detail_link'] = 'supplier_detail'
@@ -276,7 +263,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin):
form_class = forms.SupplierForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(SupplierCreate, self).get_context_data(**kwargs)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
@@ -322,8 +309,8 @@ class CableTypeDetail(generic.DetailView):
template_name = 'cable_type_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"Cable Type {self.object}"
context = super(CableTypeDetail, self).get_context_data(**kwargs)
context["page_title"] = "Cable Type {}".format(str(self.object))
return context
@@ -333,7 +320,7 @@ class CableTypeCreate(generic.CreateView):
form_class = forms.CableTypeForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(CableTypeCreate, self).get_context_data(**kwargs)
context["create"] = True
context["page_title"] = "Create Cable Type"
@@ -349,9 +336,9 @@ class CableTypeUpdate(generic.UpdateView):
form_class = forms.CableTypeForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(CableTypeUpdate, self).get_context_data(**kwargs)
context["edit"] = True
context["page_title"] = f"Edit Cable Type {self.object}"
context["page_title"] = "Edit Cable Type"
return context
@@ -359,82 +346,35 @@ class CableTypeUpdate(generic.UpdateView):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
def generate_label(pk):
black = (0, 0, 0)
white = (255, 255, 255)
size = (700, 200)
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = f"Asset: {obj.asset_id}"
if obj.is_cable:
length = f"Length: {obj.length}m"
csa = f"CSA: {obj.csa}mm²"
image = Image.new("RGB", size, white)
logo = Image.open("static/imgs/square_logo.png")
draw = ImageDraw.Draw(image)
draw.text((210, 140), asset_id, fill=black, font=font)
if obj.is_cable:
draw.text((210, 170), length, fill=black, font=font)
draw.text((360, 170), csa, fill=black, font=font)
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
return image
class GenerateLabel(generic.View): # TODO Caching
class GenerateLabel(generic.View):
def get(self, request, pk):
black = (0, 0, 0)
white = (255, 255, 255)
size = (700, 200)
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = "Asset: {}".format(obj.asset_id)
length = "Length: {}m".format(obj.length)
csa = "CSA: {}mm²".format(obj.csa)
image = Image.new("RGB", size, white)
logo = Image.open("static/imgs/square_logo.png")
draw = ImageDraw.Draw(image)
draw.text((210, 140), asset_id, fill=black, font=font)
draw.text((210, 170), length, fill=black, font=font)
draw.text((350, 170), csa, fill=black, font=font)
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
response = HttpResponse(content_type="image/png")
generate_label(pk).save(response, "PNG")
return response
class GenerateLabels(generic.View):
def get(self, request, ids):
response = HttpResponse(content_type='application/pdf')
template = get_template('labels_print.xml')
images = []
for asset_id in ids:
image = generate_label(asset_id)
in_mem_file = BytesIO()
image.save(in_mem_file, format="PNG")
# reset file pointer to start
in_mem_file.seek(0)
img_bytes = in_mem_file.read()
base64_encoded_result_bytes = base64.b64encode(img_bytes)
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
images.append(base64_encoded_result_str)
context = {
'images0': images[::4],
'images1': images[1::4],
'images2': images[2::4],
'images3': images[3::4],
'filename': "Asset Label Sheet generated at {}".format(timezone.now())
}
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
merged = BytesIO()
merger.write(merged)
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(merged.getvalue())
image.save(response, "PNG")
return response

View File

@@ -2,7 +2,9 @@ from django.conf import settings
import django
import pytest
from django.core.management import call_command
from RIGS.models import VatRate
from RIGS.models import VatRate, Profile
import random
from django.db import connection
from PyRIGS.tests import pages
import os
from selenium import webdriver

View File

@@ -3,7 +3,7 @@
var gulp = require('gulp');
const terser = require('gulp-uglify');
const sass = require('gulp-sass')(require('node-sass'));
const sass = require('gulp-sass');
const flatten = require('gulp-flatten');
const autoprefixer = require('autoprefixer')
const postcss = require('gulp-postcss')
@@ -15,6 +15,8 @@ const cssnano = require('cssnano');
const con = require('gulp-concat');
const gulpif = require('gulp-if');
sass.compiler = require('node-sass');
function fonts(done) {
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*')
.pipe(gulp.dest('pipeline/built_assets/fonts'))
@@ -27,9 +29,7 @@ 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/flatpickr/dist/flatpickr.css',
'node_modules/simplemde/dist/simplemde.min.css'
])
'node_modules/flatpickr/dist/flatpickr.css',])
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
.pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css')))
@@ -64,7 +64,6 @@ function scripts() {
'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/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')))

7782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,37 +5,35 @@
"author": "Tom Price",
"license": "Custom",
"dependencies": {
"@forevolve/bootstrap-dark": "^2.1.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@forevolve/bootstrap-dark": "^1.0.0-alpha.1075",
"@fortawesome/fontawesome-free": "^5.15.2",
"ajax-bootstrap-select": "^1.4.5",
"autocompleter": "^6.1.2",
"autoprefixer": "^10.4.0",
"autocompleter": "^6.0.3",
"autoprefixer": "^9.8.0",
"bootstrap": "^4.5.2",
"bootstrap-select": "^1.13.17",
"clipboard": "^2.0.8",
"cssnano": "^5.0.13",
"clipboard": "^2.0.6",
"cssnano": "^4.1.10",
"flatpickr": "^4.6.6",
"fullcalendar": "^5.10.1",
"fullcalendar": "^5.3.2",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0",
"gulp-if": "^3.0.0",
"gulp-postcss": "^9.0.1",
"gulp-sass": "^5.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-postcss": "^8.0.0",
"gulp-sass": "^4.1.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-uglify": "^3.0.2",
"html5sortable": "^0.13.3",
"jquery": "^3.6.0",
"konami": "^1.6.3",
"html5sortable": "^0.10.0",
"jquery": "^3.5.1",
"konami": "^1.6.2",
"moment": "^2.27.0",
"node-sass": "^7.0.0",
"node-sass": "^5.0.0",
"popper.js": "^1.16.1",
"postcss": "^8.4.5",
"simplemde": "^1.11.2",
"uglify-js": "^3.14.5"
"uglify-js": "^3.12.6"
},
"devDependencies": {
"browser-sync": "^2.27.7"
"browser-sync": "^2.26.12"
},
"scripts": {
"gulp": "gulp",

View File

@@ -1,7 +1,3 @@
marked.setOptions({
breaks: true,
})
function setupItemTable(items_json) {
objectitems = JSON.parse(items_json)
$.each(objectitems, function (key, val) {
@@ -10,12 +6,12 @@ function setupItemTable(items_json) {
newitem = -1;
}
function nl2br(str, is_xhtml) {
function nl2br (str, is_xhtml) {
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
}
function escapeHtml(str) {
function escapeHtml (str) {
return $('<div/>').text(str).html();
}
@@ -36,16 +32,6 @@ function updatePrices() {
$('#total').text(parseFloat(sum + vat).toFixed(2));
}
function setupMDE(selector) {
editor = new SimpleMDE({
element: $(selector)[0],
forceSync: true,
toolbar: ["bold", "italic", "strikethrough", "|", "unordered-list", "ordered-list", "|", "link", "|", "preview", "guide"],
status: true,
});
$(selector).data('mde_editor',editor);
}
$('#item-table').on('click', '.item-delete', function () {
delete objectitems[$(this).data('pk')]
$('#item-' + $(this).data('pk')).remove();
@@ -120,7 +106,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(marked(fields.description));
$row.find('.description').html(nl2br(escapeHtml(fields.description)));
$row.find('.cost').html(parseFloat(fields.cost).toFixed(2));
$row.find('.quantity').html(fields.quantity);

View File

@@ -28,9 +28,6 @@
color: $gray-100 !important;
border-color: $darktheme;
}
.btn-link {
color: white;
}
.bs-popover-right > .arrow::after {
border-right-color: $darktheme;
}
@@ -136,21 +133,4 @@
-webkit-box-shadow: 0 0 0px 1000px rgba($info, .3) inset;
transition: background-color 5000s ease-in-out 0s;
}
.editor-toolbar > a {
color: white !important;
}
.editor-toolbar > a:hover {
background: transparent !important;
}
.editor-toolbar > a.active {
background: $info !important;
}
.cm-s-paper {
color: white;
background-color: $darktheme;
border-color: #bbb;
}
.CodeMirror-cursor {
border-color: white !important;
}
}

View File

@@ -226,33 +226,3 @@ html.embedded {
max-width: 3em;
}
}
.markdown {
h1 {
font-size: $h1-font-size * 0.75;
}
h2 {
font-size: $h2-font-size * 0.8;
}
h3 {
font-size: $h3-font-size * 0.85;
}
h4 {
font-size: $h4-font-size * 0.9;
}
h5 {
font-size: $h5-font-size * 0.95;
}
img {
max-width: 100%;
}
}
#rigboard {
.markdown {
img {
max-width: 30rem;
}
}
}

View File

@@ -33,7 +33,7 @@
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
<div class="container">
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://members.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
<img src="{% static 'imgs/logo.webp' %}" width="40" height="40" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo">
</a>
{% block titleheader %}
@@ -77,11 +77,8 @@
</div>
<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 %}
{% endblock %}
</body>

View File

@@ -43,7 +43,7 @@
<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>
<a class="list-group-item list-group-item-action text-info" href="{% url 'trainee_detail' %}"><span class="fas fa-file-signature align-middle"></span><span class="align-middle"> My Training Record</span></a>
<a class="list-group-item list-group-item-action" href="{% url 'trainee_list' %}"><span class="fas fa-users"></span> Trainee List</a>
<a class="list-group-item list-group-item-action" href="{% url 'level_list' %}"><span class="fas fa-layer-group"></span> Level List</a></a>
<a class="list-group-item list-group-item-action" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a></a>

View File

@@ -2,8 +2,6 @@
<button type="submit" class="btn {{ class }}" title="{{ text }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></button>
{% elif pk %}
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
{% elif copy %}
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
{% else %}
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
{% endif %}

View File

@@ -2,10 +2,7 @@ from django.contrib import admin
from training import models
from reversion.admin import VersionAdmin
# admin.site.register(models.Trainee, VersionAdmin)
#admin.site.register(models.Trainee, VersionAdmin)
admin.site.register(models.TrainingCategory, VersionAdmin)
admin.site.register(models.TrainingItem, VersionAdmin)
admin.site.register(models.TrainingLevel, VersionAdmin)
admin.site.register(models.TrainingItemQualification, VersionAdmin)
admin.site.register(models.TrainingLevelQualification, VersionAdmin)
admin.site.register(models.TrainingLevelRequirement, VersionAdmin)

View File

@@ -1,5 +0,0 @@
from PyRIGS.decorators import user_passes_test_with_403
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)

View File

@@ -1,9 +1,15 @@
from django import forms
from datetime import date
from training import models
from RIGS.models import Profile
class SessionLogForm(forms.Form):
pass
class QualificationForm(forms.ModelForm):
class Meta:
model = models.TrainingItemQualification
@@ -11,9 +17,9 @@ class QualificationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None)
super().__init__(*args, **kwargs)
super(QualificationForm, self).__init__(*args, **kwargs)
self.fields['trainee'].initial = Profile.objects.get(pk=pk)
self.fields['date'].widget.format = '%Y-%m-%d'
self.fields['date'].initial = date.today()
def clean_date(self):
date = self.cleaned_data['date']
@@ -39,5 +45,5 @@ class RequirementForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None)
super().__init__(*args, **kwargs)
super(RequirementForm, self).__init__(*args, **kwargs)
self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk)

View File

@@ -2,7 +2,6 @@ import datetime
import random
from django.contrib.auth.models import Group, Permission
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
@@ -32,7 +31,7 @@ class Command(BaseCommand):
self.setup_categories()
self.setup_items()
self.setup_levels()
# call_command('generate_sample_training_users')
self.setup_supervisor()
print("Done generating training data")
def setup_categories(self):
@@ -44,148 +43,32 @@ class Command(BaseCommand):
self.categories.append(category)
def setup_items(self):
names = [
"Motorised Power Towers",
"Catering",
"Forgetting Cables",
"Gazebo Construction",
"Balanced Audio",
"Unbalanced Audio",
"BBQ/Bin Interactions",
"Pushing Boxes",
"How Not To Die",
"Setting up projectors",
"Basketing truss",
"First Aid",
"Digging Trenches",
"Avoiding Bin Lorries",
"Getting cherry pickers stuck in mud",
"Crashing the Van",
"Getting pigs to fly",
"Basketing picnics",
"Python programming",
"Building Cables",
"Unbuilding Cables",
"Cat Herding",
"Pancake making",
"Tidying up",
"Reading Manuals",
"Bikeshedding",
"DJing",
"Partying",
"Teccie Gym",
"Putting dust covers on",
"Cleaning Lights",
"Water Skiing",
"Drinking",
"Fundamentals of Audio",
"Fundamentals of Photons",
"Social Interaction",
"Discourse Searching",
"Discord Searching",
"Coiling Cables",
"Kit Amnesties",
"Van Insurance",
"Subhire Insurance",
"Paperwork",
"More Paperwork",
"Second Aid",
"Being Old",
"Maxihoists",
"Sleazyhoists",
"Telehoists",
"Prolyte",
"Prolights",
"Making Phonecalls",
"Quoting For A Rig",
"Basic MIC",
"Advanced MIC",
"Avoiding MIC",
"Washing Cables",
"Cable Ramp",
"Van Loading",
"Trailer Loading",
"Storeroom Loading",
"Welding",
"Fire Extinguishers",
"Boring Conference AV",
"Flyaway",
"Short Leads",
"RF Systems",
"QLab",
"Use of Ladders",
"Working at Height",
"Organising Training",
"Organising Organising Training Training",
"Mental Health First Aid",
"Writing RAMS",
"Makros Runs",
"PAT",
"Kit Fixing",
"Kit Breaking",
"Replacing Lamps",
"Flying Pig Systems",
"Procrastination",
"Drinking Beer",
"Sending Emails",
"Email Signatures",
"Digital Sound Desks",
"Digital Lighting Desks",
"Painting PS10s",
"Chain Lubrication",
"Big Power",
"BIGGER POWER",
"Pixel Mapping",
"RDM",
"Ladder Inspections",
"Losing Crimpaz",
"Scrapping Trilite",
"Bin Diving",
"Wiki Editing"]
names = ["Motorised Power Towers", "Catering", "Forgetting Cables", "Gazebo Construction", "Balanced Audio", "Unbalanced Audio", "BBQ/Bin Interactions", "Pushing Boxes", "How Not To Die", "Setting up projectors", "Basketing truss", "First Aid", "Digging Trenches", "Avoiding Bin Lorries", "Getting cherry pickers stuck in mud", "Crashing the Van", "Getting pigs to fly", "Basketing picnics", "Python programming", "Building Cables", "Unbuilding Cables", "Cat Herding", "Pancake making", "Tidying up", "Reading Manuals", "Bikeshedding", "DJing", "Partying", "Teccie Gym", "Putting dust covers on", "Cleaning Lights", "Water Skiing", "Drinking", "Fundamentals of Audio", "Fundamentals of Photons", "Social Interaction", "Discourse Searching", "Discord Searching", "Coiling Cables", "Kit Amnesties", "Van Insurance", "Subhire Insurance", "Paperwork", "More Paperwork", "Second Aid", "Being Old", "Maxihoists", "Sleazyhoists", "Telehoists", "Prolyte", "Prolights", "Making Phonecalls", "Quoting For A Rig", "Basic MIC", "Advanced MIC", "Avoiding MIC", "Washing Cables", "Cable Ramp", "Van Loading", "Trailer Loading", "Storeroom Loading", "Welding", "Fire Extinguishers", "Boring Conference AV", "Flyaway", "Short Leads", "RF Systems", "QLab", "Use of Ladders", "Working at Height", "Organising Training", "Organising Organising Training Training", "Mental Health First Aid", "Writing RAMS", "Makros Runs", "PAT", "Kit Fixing", "Kit Breaking", "Replacing Lamps", "Flying Pig Systems", "Procrastination", "Drinking Beer", "Sending Emails", "Email Signatures", "Digital Sound Desks", "Digital Lighting Desks", "Painting PS10s", "Chain Lubrication", "Big Power", "BIGGER POWER", "Pixel Mapping", "RDM", "Ladder Inspections", "Losing Crimpaz", "Scrapping Trilite", "Bin Diving", "Wiki Editing"]
for i, name in enumerate(names):
category = random.choice(self.categories)
previous_item = models.TrainingItem.objects.filter(category=category).last()
if previous_item is not None:
number = previous_item.reference_number + 1
else:
number = 0
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name)
for i,name in enumerate(names):
item = models.TrainingItem.objects.create(category=random.choice(self.categories), reference_number=random.randint(0, 100), name=name)
self.items.append(item)
def setup_levels(self):
items = self.items.copy()
ta = models.TrainingLevel.objects.create(
level=models.TrainingLevel.TA,
description="Passion will hatred faithful evil suicide noble battle. Truth aversion gains grandeur noble. Dead play gains prejudice god ascetic grandeur zarathustra dead good. Faithful ultimate justice overcome love will mountains inexpedient.",
icon="address-card")
ta = models.TrainingLevel.objects.create(level=models.TrainingLevel.TA, description="Passion will hatred faithful evil suicide noble battle. Truth aversion gains grandeur noble. Dead play gains prejudice god ascetic grandeur zarathustra dead good. Faithful ultimate justice overcome love will mountains inexpedient.")
self.levels.append(ta)
tech_ccs = models.TrainingLevel.objects.create(
level=models.TrainingLevel.TECHNICIAN,
description="Technician Common Competencies. Spirit abstract endless insofar horror sexuality depths war decrepit against strong aversion revaluation free. Christianity reason joy sea law mountains transvaluation. Sea battle aversion dead ultimate morality self. Faithful morality.",
icon="book-reader")
tech_ccs = models.TrainingLevel.objects.create(level=models.TrainingLevel.TECHNICIAN, description="Technician Common Competencies. Spirit abstract endless insofar horror sexuality depths war decrepit against strong aversion revaluation free. Christianity reason joy sea law mountains transvaluation. Sea battle aversion dead ultimate morality self. Faithful morality.")
tech_ccs.prerequisite_levels.add(ta)
super_ccs = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, description="Depths disgust hope faith of against hatred will victorious. Law...", icon="user-graduate")
super_ccs = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, description="Depths disgust hope faith of against hatred will victorious. Law...")
for i in range(0, 5):
if len(items) == 0:
break
item = random.choice(items)
items.remove(item)
if i % 3 == 0:
models.TrainingLevelRequirement.objects.create(level=tech_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
else:
models.TrainingLevelRequirement.objects.create(level=super_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
icons = {
models.TrainingLevel.SOUND: ('microphone', 'microphone-alt'),
models.TrainingLevel.LIGHTING: ('lightbulb', 'traffic-light'),
models.TrainingLevel.POWER: ('plug', 'bolt'),
models.TrainingLevel.RIGGING: ('link', 'pallet'),
models.TrainingLevel.HAULAGE: ('truck', 'route'),
}
for i, name in models.TrainingLevel.DEPARTMENTS:
technician = models.TrainingLevel.objects.create(level=models.TrainingLevel.TECHNICIAN, department=i, description="Moral pinnacle derive ultimate war dead. Strong fearful joy contradict battle christian faithful enlightenment prejudice zarathustra moral.", icon=icons[i][0])
if len(items) == 0:
break
item = random.choice(items)
items.remove(item)
if i % 3 == 0:
models.TrainingLevelRequirement.objects.create(level=tech_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
else:
models.TrainingLevelRequirement.objects.create(level=super_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
for i,name in models.TrainingLevel.DEPARTMENTS:
technician = models.TrainingLevel.objects.create(level=models.TrainingLevel.TECHNICIAN, department=i, description="Moral pinnacle derive ultimate war dead. Strong fearful joy contradict battle christian faithful enlightenment prejudice zarathustra moral.")
technician.prerequisite_levels.add(tech_ccs)
supervisor = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, department=i, description="Spirit holiest merciful mountains inexpedient reason value. Suicide ultimate hope.", icon=icons[i][1])
supervisor = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, department=i, description="Spirit holiest merciful mountains inexpedient reason value. Suicide ultimate hope.")
supervisor.prerequisite_levels.add(super_ccs, technician)
for i in range(0, 30):
@@ -193,13 +76,18 @@ class Command(BaseCommand):
break
item = random.choice(items)
items.remove(item)
try:
if i % 3 == 0:
models.TrainingLevelRequirement.objects.create(level=technician, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
else:
models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
except: # noqa
print("Failed create for {}. Weird.".format(item))
if i % 3 == 0:
models.TrainingLevelRequirement.objects.create(level=technician, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
else:
models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
self.levels.append(technician)
self.levels.append(supervisor)
def setup_supervisor(self):
supervisor = models.Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV",
email="supervisor@example.com", is_active=True,
is_staff=True)
supervisor.set_password('supervisor')
supervisor.save()
models.TrainingLevelQualification.objects.create(trainee=supervisor, level=models.TrainingLevel.objects.filter(level__gte=models.TrainingLevel.SUPERVISOR).exclude(department=models.TrainingLevel.HAULAGE).exclude(department__isnull=True).first(), confirmed_on=timezone.now())

View File

@@ -1,77 +0,0 @@
import datetime
import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from reversion import revisions as reversion
from training import models
from RIGS.models import Profile
class Command(BaseCommand):
help = 'Adds training users'
can_import_settings = True
profiles = []
committee_group = None
def handle(self, *args, **options):
print("Generating useful training users")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('otherwise it is done by time, which could lead to inconsistent tests')
with transaction.atomic():
self.setup_groups()
self.setup_useful_profiles()
print("Done generating useful training users")
def setup_groups(self):
self.committee_group = Group.objects.create(name='Committee')
perms = [
"add_trainingitemqualification",
"change_trainingitemqualification",
"delete_trainingitemqualification",
"add_traininglevelqualification",
"change_traininglevelqualification",
"delete_traininglevelqualification",
"add_traininglevelrequirement",
"change_traininglevelrequirement",
"delete_traininglevelrequirement"]
for permId in perms:
self.committee_group.permissions.add(Permission.objects.get(codename=permId))
self.committee_group.save()
def setup_useful_profiles(self):
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV",
email="supervisor@example.com", is_active=True,
is_staff=True, is_approved=True)
supervisor.set_password('supervisor')
supervisor.groups.add(Group.objects.get(name="Keyholders"))
supervisor.save()
models.TrainingLevelQualification.objects.create(
trainee=supervisor,
level=models.TrainingLevel.objects.filter(
level__gte=models.TrainingLevel.SUPERVISOR).exclude(
department=models.TrainingLevel.HAULAGE).exclude(
department__isnull=True).first(),
confirmed_on=timezone.now(),
confirmed_by=models.Trainee.objects.first())
committee_user = Profile.objects.create(username="committee", first_name="Committee", last_name="Member",
initials="CM",
email="committee@example.com", is_active=True, is_approved=True)
committee_user.groups.add(self.committee_group)
supervisor.groups.add(Group.objects.get(name="Keyholders"))
committee_user.set_password('committee')
committee_user.save()

View File

@@ -1,282 +0,0 @@
import os
import datetime
import re
import xml.etree.ElementTree as ET
from django.core.management.base import BaseCommand
from django.conf import settings
from django.db.utils import IntegrityError
from django.utils.timezone import make_aware
from training import models
from RIGS.models import Profile
class Command(BaseCommand):
epoch = datetime.date(1970, 1, 1)
id_map = {}
def handle(self, *args, **options):
self.import_Trainees()
self.import_TrainingCatagory()
self.import_TrainingItem()
self.import_TrainingItemQualification()
self.import_TrainingLevel()
self.import_TrainingLevelQualification()
self.import_TrainingLevelRequirements()
@staticmethod
def xml_path(file):
return os.path.join(settings.BASE_DIR, 'data/{}'.format(file))
@staticmethod
def parse_xml(file):
tree = ET.parse(file)
return tree.getroot()
def import_Trainees(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Members.xml'))
for child in root:
try:
name = child.find('Member_x0020_Name').text
first_name = name.split()[0]
last_name = " ".join(name.split()[1:])
profile = Profile.objects.filter(first_name=first_name, last_name=last_name).first()
if profile:
self.id_map[child.find('ID').text] = profile.pk
print(f"Found existing user {profile}, matching data")
tally[0] += 1
else:
# PYTHONIC, BABY
initials = first_name[0] + "".join([name_section[0] for name_section in re.split("\\s*-", last_name.replace("(", ""))])
# print(initials)
new_profile = Profile.objects.create(username=name.replace(" ", ""),
first_name=first_name,
last_name=last_name,
initials=initials)
self.id_map[child.find('ID').text] = new_profile.pk
tally[1] += 1
print(f"No match found, creating new user {new_profile}")
except AttributeError: # W.T.F
print("Trainee #{} is FUBAR".format(child.find('ID').text))
print('Trainees - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingCatagory(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Categories.xml'))
for child in root:
obj, created = models.TrainingCategory.objects.update_or_create(
pk=int(child.find('ID').text),
reference_number=int(child.find('Category_x0020_Number').text),
name=child.find('Category_x0020_Name').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingItem(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Items.xml'))
for child in root:
if child.find('active').text == '0':
active = False
else:
active = True
number = int(child.find('Item_x0020_Number').text)
name = child.find('Item_x0020_Name').text
category = models.TrainingCategory.objects.get(pk=int(child.find('Category_x0020_ID').text))
try:
obj, created = models.TrainingItem.objects.update_or_create(
pk=int(child.find('ID').text),
reference_number=number,
name=name,
category=category,
active=active
)
except IntegrityError:
print("Training Item {}.{} {} has a duplicate reference number".format(category.reference_number, number, name))
if created:
tally[1] += 1
else:
tally[0] += 1
print('Training Items - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingItemQualification(self):
tally = [0, 0, 0]
root = self.parse_xml(self.xml_path('Training Records.xml'))
for child in root:
depths = [("Training_Started", models.TrainingItemQualification.STARTED),
("Training_Complete", models.TrainingItemQualification.COMPLETE),
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
for (depth, depth_index) in depths:
if child.find('{}_Date'.format(depth)) is not None:
if child.find('{}_Assessor_ID'.format(depth)) is None:
print("Training Record #{} had no supervisor. Assigning System User.".format(child.find('ID').text))
supervisor = Profile.objects.get(first_name="God")
continue
supervisor = Profile.objects.get(pk=self.id_map[child.find('{}_Assessor_ID'.format(depth)).text])
if child.find('Member_ID') is None:
print("Training Record #{} didn't train anybody and has been ignored. Dammit {}".format(child.find('ID').text, supervisor.name))
tally[2] += 1
continue
try:
obj, created = models.TrainingItemQualification.objects.update_or_create(
item=models.TrainingItem.objects.get(pk=int(child.find('Training_Item_ID').text)),
trainee=Profile.objects.get(pk=self.id_map[child.find('Member_ID').text]),
depth=depth_index,
date=child.find('{}_Date'.format(depth)).text[:-9], # Stored as datetime with time as midnight because fuck you I guess
supervisor=supervisor
)
notes = child.find('{}_Notes'.format(depth))
if notes is not None:
obj.notes = notes.text
obj.save()
if created:
tally[1] += 1
else:
tally[0] += 1
except IntegrityError: # Eh?
print("Training Record #{} is probably duplicate. ಠ_ಠ".format(child.find('ID').text))
except AttributeError:
print(child.find('ID').text)
print('Training Item Qualifications - Updated: {}, Created: {}, Broken: {}'.format(tally[0], tally[1], tally[2]))
def import_TrainingLevel(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Levels.xml'))
for child in root:
name = child.find('Level_x0020_Name').text
if name == "Technical Assistant":
level = models.TrainingLevel.TA
depString = None
elif "Common" in name:
levelString = name.split()[0]
if levelString == "Technician":
level = models.TrainingLevel.TECHNICIAN
elif levelString == "Supervisor":
level = models.TrainingLevel.SUPERVISOR
depString = None
else:
depString = name.split()[-1]
levelString = name.split()[0]
if levelString == "Technician":
level = models.TrainingLevel.TECHNICIAN
elif levelString == "Supervisor":
level = models.TrainingLevel.SUPERVISOR
else:
print(levelString)
continue
for dep in models.TrainingLevel.DEPARTMENTS:
if dep[1] == depString:
department = dep[0]
desc = ""
if child.find('Desc') is not None:
desc = child.find('Desc').text
obj, created = models.TrainingLevel.objects.update_or_create(
pk=int(child.find('ID').text),
description=desc,
level=level
)
if depString is not None:
obj.department = department
obj.save()
if created:
tally[1] += 1
else:
tally[0] += 1
for level in models.TrainingLevel.objects.all():
if level.department is not None:
if level.level == models.TrainingLevel.TECHNICIAN:
level.prerequisite_levels.add(models.TrainingLevel.objects.get(level=models.TrainingLevel.TA), models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=None))
elif level.level == models.TrainingLevel.SUPERVISOR:
level.prerequisite_levels.add(models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=level.department), models.TrainingLevel.objects.get(level=models.TrainingLevel.SUPERVISOR, department=None))
print('Training Levels - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingLevelQualification(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Level Records.xml'))
for child in root:
try:
trainee = Profile.objects.get(pk=self.id_map[child.find('Member_x0020_ID').text]) if child.find('Member_x0020_ID') is not None else False
level = models.TrainingLevel.objects.get(pk=int(child.find('Training_x0020_Level_x0020_ID').text)) if child.find('Training_x0020_Level_x0020_ID') is not None else False
if trainee and level:
obj, created = models.TrainingLevelQualification.objects.update_or_create(pk=int(child.find('ID').text),
trainee=trainee,
level=level)
else:
print('Training Level Qualification #{} failed to import. Trainee: {} and Level: {}'.format(child.find('ID').text, trainee, level))
continue
if child.find('Date_x0020_Level_x0020_Awarded') is not None:
obj.confirmed_on = make_aware(datetime.datetime.strptime(child.find('Date_x0020_Level_x0020_Awarded').text.split('T')[0], "%Y-%m-%d"))
obj.save()
# confirmed by?
if created:
tally[1] += 1
else:
tally[0] += 1
except IntegrityError: # Eh?
print("Training Level Qualification #{} is duplicate. ಠ_ಠ".format(child.find('ID').text))
print('TrainingLevelQualifications - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingLevelRequirements(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Level Requirements.xml'))
for child in root:
items = child.find('Items').text.split(",")
for item in items:
try:
item = item.split('.')
obj, created = models.TrainingLevelRequirement.objects.update_or_create(
level=models.TrainingLevel.objects.get(
pk=int(
child.find('Level').text)), item=models.TrainingItem.objects.get(
active=True, reference_number=item[1], category=models.TrainingCategory.objects.get(
reference_number=item[0])), depth=int(
child.find('Depth').text))
if created:
tally[1] += 1
else:
tally[0] += 1
except models.TrainingItem.DoesNotExist:
print("Item with number {} does not exist".format(item))
except models.TrainingItem.MultipleObjectsReturned:
print(models.TrainingItem.objects.filter(reference_number=item[1], category=models.TrainingCategory.objects.get(reference_number=item[0])))
print('TrainingLevelRequirements - Updated: {}, Created: {}'.format(tally[0], tally[1]))

View File

@@ -1,4 +1,4 @@
# Generated by Django 3.2.11 on 2022-01-04 20:08
# Generated by Django 3.1.5 on 2021-07-05 22:01
import RIGS.models
import django.contrib.auth.models
@@ -11,7 +11,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('RIGS', '0043_auto_20211027_1519'),
('RIGS', '0041_auto_20210302_1204'),
]
operations = [
@@ -19,36 +19,25 @@ class Migration(migrations.Migration):
name='TrainingCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference_number', models.IntegerField(unique=True)),
('reference_number', models.CharField(max_length=3)),
('name', models.CharField(max_length=50)),
],
options={
'verbose_name_plural': 'Training Categories',
},
),
migrations.CreateModel(
name='TrainingItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference_number', models.IntegerField()),
('reference_number', models.CharField(max_length=3)),
('name', models.CharField(max_length=50)),
('active', models.BooleanField(default=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='training.trainingcategory')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='items', to='training.trainingcategory')),
],
options={
'ordering': ['category__reference_number', 'reference_number'],
'unique_together': {('reference_number', 'active', 'category')},
},
),
migrations.CreateModel(
name='TrainingLevel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True)),
('department', models.IntegerField(blank=True, choices=[(0, 'Sound'), (1, 'Lighting'), (2, 'Power'), (3, 'Rigging'), (4, 'Haulage')], null=True)),
('department', models.CharField(max_length=50, null=True)),
('level', models.IntegerField(choices=[(0, 'Technical Assistant'), (1, 'Technician'), (2, 'Supervisor')])),
('icon', models.CharField(blank=True, max_length=20, null=True)),
('prerequisite_levels', models.ManyToManyField(blank=True, related_name='prerequisites', to='training.TrainingLevel')),
],
bases=(models.Model, RIGS.models.RevisionMixin),
),
@@ -61,38 +50,20 @@ class Migration(migrations.Migration):
'indexes': [],
'constraints': [],
},
bases=('RIGS.profile', RIGS.models.RevisionMixin),
bases=('RIGS.profile',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='TrainingLevelRequirement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('depth', models.IntegerField(choices=[(0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')])),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.trainingitem')),
('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requirements', to='training.traininglevel')),
],
options={
'unique_together': {('level', 'item')},
},
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='TrainingLevelQualification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('confirmed_on', models.DateTimeField(null=True)),
('confirmed_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='confirmer', to='training.trainee')),
('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel')),
('trainee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='level_qualifications', to='training.trainee')),
('confirmed_on', models.DateTimeField()),
('confirmed_by', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='confirmer', to='training.trainee')),
('level', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='training.traininglevel')),
('trainee', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='levels', to='training.trainee')),
],
options={
'ordering': ['-confirmed_on'],
'unique_together': {('trainee', 'level')},
},
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='TrainingItemQualification',
@@ -101,13 +72,9 @@ class Migration(migrations.Migration):
('depth', models.IntegerField(choices=[(0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')])),
('date', models.DateField()),
('notes', models.TextField(blank=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.trainingitem')),
('supervisor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qualifications_granted', to='training.trainee')),
('trainee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qualifications_obtained', to='training.trainee')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='training.trainingitem')),
('supervisor', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='qualifications_granted', to='training.trainee')),
('trainee', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='qualifications_obtained', to='training.trainee')),
],
options={
'order_with_respect_to': 'item',
'unique_together': {('trainee', 'item', 'depth')},
},
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 3.2.11 on 2022-01-05 12:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='traininglevel',
options={'ordering': ['department', 'level']},
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 3.1.5 on 2021-07-05 23:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('training', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='trainingcategory',
options={'verbose_name_plural': 'Training Categories'},
),
migrations.AddField(
model_name='traininglevel',
name='description',
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name='traininglevel',
name='prerequisite_levels',
field=models.ManyToManyField(blank=True, related_name='prerequisites', to='training.TrainingLevel'),
),
migrations.AlterField(
model_name='traininglevel',
name='department',
field=models.IntegerField(choices=[(0, 'Sound'), (1, 'Lighting'), (2, 'Power'), (3, 'Rigging'), (4, 'Haulage')], null=True),
),
migrations.CreateModel(
name='TrainingLevelRequirement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('depth', models.IntegerField(verbose_name=((0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')))),
('item', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='training.trainingitem')),
('level', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='requirements', to='training.traininglevel')),
],
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.5 on 2021-07-16 00:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('training', '0002_auto_20210706_0053'),
]
operations = [
migrations.AlterField(
model_name='traininglevelqualification',
name='confirmed_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='confirmer', to='training.trainee'),
),
migrations.AlterField(
model_name='traininglevelqualification',
name='confirmed_on',
field=models.DateTimeField(null=True),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.7 on 2021-08-19 17:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0003_auto_20210716_0150'),
]
operations = [
migrations.AlterUniqueTogether(
name='trainingitemqualification',
unique_together={('trainee', 'item', 'depth')},
),
migrations.AlterUniqueTogether(
name='traininglevelqualification',
unique_together={('trainee', 'level')},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-08-19 17:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0004_auto_20210819_1808'),
]
operations = [
migrations.AlterUniqueTogether(
name='traininglevelrequirement',
unique_together={('level', 'item')},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-09-03 20:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('training', '0005_auto_20210819_1833'),
]
operations = [
migrations.AddField(
model_name='traininglevel',
name='icon',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AlterField(
model_name='traininglevelrequirement',
name='depth',
field=models.IntegerField(choices=[(0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')]),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.13 on 2021-09-08 19:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0006_auto_20210903_2158'),
]
operations = [
migrations.AlterModelOptions(
name='trainingitem',
options={'ordering': ['category__reference_number', 'reference_number']},
),
migrations.AlterUniqueTogether(
name='trainingitem',
unique_together={('reference_number', 'name', 'category')},
),
migrations.AlterOrderWithRespectTo(
name='trainingitemqualification',
order_with_respect_to='item',
),
]

View File

@@ -1,83 +1,85 @@
from django.db import models
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
# 'shim' overtop the profile model to neatly contain all training related fields etc
@reversion.register(for_concrete_model=False, fields=[], follow=["qualifications_obtained", "level_qualifications"])
@reversion.register # profile is already registered, but this triggers my custom versioning logic
class Trainee(Profile, RevisionMixin):
class Meta:
proxy = True
# 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)]
return [level for level in TrainingLevel.objects.all() if level.percentage_complete(self) > 0]
def level_qualifications(self, only_confirmed=False):
levels = self.levels.all()
if only_confirmed:
levels = levels.exclude(confirmed_on__isnull=True)
return levels.select_related('level')
@property
def is_technician(self):
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
.filter(level__level=TrainingLevel.TECHNICIAN) \
def is_supervisor(self):
return self.level_qualifications(True) \
.filter(level__gte=TrainingLevel.SUPERVISOR) \
.exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists()
@property
def is_driver(self):
return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level').filter(level__department=TrainingLevel.HAULAGE).exists()
return self.level_qualifications(True).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).first() is not None # this is a somewhat ghetto version of get_or_none
qual = self.qualifications_obtained.filter(item=item).first() # this is a somewhat ghetto version of get_or_none
return qual is not None and qual.depth >= required_depth
def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.pk})
@property
def display_id(self):
return str(self)
# Items
class TrainingCategory(models.Model):
reference_number = models.IntegerField(unique=True)
reference_number = models.CharField(max_length=3)
name = models.CharField(max_length=50)
def __str__(self):
return f"{self.reference_number}. {self.name}"
return "{}. {}".format(self.reference_number, self.name)
class Meta:
verbose_name_plural = 'Training Categories'
@reversion.register
class TrainingItem(models.Model):
reference_number = models.IntegerField()
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
reference_number = models.CharField(max_length=3)
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.RESTRICT)
name = models.CharField(max_length=50)
active = models.BooleanField(default=True)
@property
def display_id(self):
return f"{self.category.reference_number}.{self.reference_number}"
def number(self):
return "{}.{}".format(self.category.reference_number, self.reference_number)
def __str__(self):
name = f"{self.display_id} {self.name}"
if not self.active:
name += " (inactive)"
return name
return "{} {}".format(self.number, self.name)
@staticmethod
def user_has_qualification(item, user, depth):
return user.qualifications_obtained.only('item', 'depth').filter(item=item, depth__gte=depth).exists()
for q in user.qualifications_obtained.all().select_related('item'):
if q.item == item and q.depth > depth:
return True
class Meta:
unique_together = ["reference_number", "active", "category"]
unique_together = ["reference_number", "name", "category"]
ordering = ['category__reference_number', 'reference_number']
@reversion.register
class TrainingItemQualification(models.Model, RevisionMixin):
class TrainingItemQualification(models.Model):
STARTED = 0
COMPLETE = 1
PASSED_OUT = 2
@@ -86,33 +88,34 @@ class TrainingItemQualification(models.Model, RevisionMixin):
(COMPLETE, 'Training Complete'),
(PASSED_OUT, 'Passed Out'),
)
item = models.ForeignKey('TrainingItem', on_delete=models.CASCADE)
item = models.ForeignKey('TrainingItem', on_delete=models.RESTRICT)
trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.RESTRICT)
depth = models.IntegerField(choices=CHOICES)
trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.CASCADE)
date = models.DateField()
# TODO Remember that some training is external. Support for making an organisation the trainer?
supervisor = models.ForeignKey('Trainee', related_name='qualifications_granted', on_delete=models.CASCADE)
supervisor = models.ForeignKey('Trainee', related_name='qualifications_granted', on_delete=models.RESTRICT)
notes = models.TextField(blank=True)
# TODO Maximum depth - some things stop at Complete and you can't be passed out in them
def __str__(self):
return "{} in {} on {}".format(self.get_depth_display(), self.item, self.date.strftime("%b %d %Y"))
return "{} in {} on {}".format(self.depth, self.item, self.date)
@property
def activity_feed_string(self):
return str("{} in {}".format(self.get_depth_display(), self.item))
def save(self, *args, **kwargs):
super().save()
for level in TrainingLevel.objects.all(): # Mm yes efficiency FIXME
if level.user_has_requirements(self.trainee):
with reversion.create_revision():
level_qualification = TrainingLevelQualification.objects.get_or_create(trainee=self.trainee, level=level)
reversion.add_to_revision(self.trainee)
@classmethod
def get_colour_from_depth(cls, obj, depth):
def get_colour_from_depth(obj, depth):
if depth == 0:
return "warning"
if depth == 1:
elif depth == 1:
return "success"
return "info"
def get_absolute_url(self):
return reverse('trainee_item_detail', kwargs={'pk': self.trainee.pk})
else:
return "info"
class Meta:
unique_together = ["trainee", "item", "depth"]
@@ -122,7 +125,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
# Levels
@reversion.register(follow=["requirements"])
class TrainingLevel(models.Model, RevisionMixin):
description = models.TextField(blank=True)
description = models.CharField(max_length=120, blank=True)
TA = 0
TECHNICIAN = 1
SUPERVISOR = 2
@@ -131,40 +134,32 @@ class TrainingLevel(models.Model, RevisionMixin):
(TECHNICIAN, 'Technician'),
(SUPERVISOR, 'Supervisor'),
)
SOUND = 0
LIGHTING = 1
POWER = 2
RIGGING = 3
HAULAGE = 4
DEPARTMENTS = (
(SOUND, 'Sound'),
(LIGHTING, 'Lighting'),
(POWER, 'Power'),
(RIGGING, 'Rigging'),
(0, 'Sound'),
(1, 'Lighting'),
(2, 'Power'),
(3, 'Rigging'),
(HAULAGE, 'Haulage'),
)
department = models.IntegerField(choices=DEPARTMENTS, null=True, blank=True) # N.B. Technical Assistant does not have a department
department = models.IntegerField(choices=DEPARTMENTS, null=True) # N.B. Technical Assistant does not have a department
level = models.IntegerField(choices=CHOICES)
prerequisite_levels = models.ManyToManyField('self', related_name='prerequisites', symmetrical=False, blank=True)
icon = models.CharField(null=True, blank=True, max_length=20)
class Meta:
ordering = ["department", "level"]
@property
def department_colour(self):
if self.department == self.SOUND:
def get_department_colour(self):
if self.department == 0:
return "info"
if self.department == self.LIGHTING:
elif self.department == 1:
return "dark"
if self.department == self.POWER:
elif self.department == 2:
return "danger"
if self.department == self.RIGGING:
elif self.department == 3:
return "warning"
if self.department == self.HAULAGE:
elif self.department == 4:
return "light"
return "primary"
else:
return "primary"
def get_requirements_of_depth(self, depth):
return self.requirements.filter(depth=depth)
@@ -185,8 +180,8 @@ class TrainingLevel(models.Model, RevisionMixin):
def passed_out_requirements(self):
return self.get_requirements_of_depth(TrainingItemQualification.PASSED_OUT)
def percentage_complete(self, user):
needed_qualifications = self.requirements.all().select_related('item')
def percentage_complete(self, user): # FIXME
needed_qualifications = self.requirements.all().select_related()
relavant_qualifications = 0.0
# TODO Efficiency...
for req in needed_qualifications:
@@ -195,14 +190,11 @@ class TrainingLevel(models.Model, RevisionMixin):
if len(needed_qualifications) > 0:
return int(relavant_qualifications / float(len(needed_qualifications)) * 100)
return 0
else:
return 0
def user_has_requirements(self, user):
has_required_items = all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.all())
# Always true if there are no prerequisites, otherwise get a set of prerequsite IDs and check if they are a subset of the set of qualification IDs
has_required_levels = not self.prerequisite_levels.all().exists() or set(self.prerequisite_levels.values_list('pk', flat=True)).issubset(set(user.level_qualifications.values_list('level', flat=True)))
return has_required_items and has_required_levels
return all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.select_related().all())
def __str__(self):
if self.department is None:
@@ -220,19 +212,11 @@ class TrainingLevel(models.Model, RevisionMixin):
def get_absolute_url(self):
return reverse('level_detail', kwargs={'pk': self.pk})
@property
def get_icon(self):
if self.icon is not None:
icon = f"<span class='fas fa-{self.icon}'></span>"
else:
icon = "".join([w[0] for w in str(self).split()])
return mark_safe("<span class='badge badge-{} badge-pill' data-toggle='tooltip' title='{}'>{}</span>".format(self.department_colour, str(self), icon))
@reversion.register
class TrainingLevelRequirement(models.Model, RevisionMixin):
level = models.ForeignKey('TrainingLevel', related_name='requirements', on_delete=models.CASCADE)
item = models.ForeignKey('TrainingItem', on_delete=models.CASCADE)
level = models.ForeignKey('TrainingLevel', related_name='requirements', on_delete=models.RESTRICT)
item = models.ForeignKey('TrainingItem', on_delete=models.RESTRICT)
depth = models.IntegerField(choices=TrainingItemQualification.CHOICES)
reversion_hide = True
@@ -246,27 +230,17 @@ class TrainingLevelRequirement(models.Model, RevisionMixin):
@reversion.register
class TrainingLevelQualification(models.Model, RevisionMixin):
trainee = models.ForeignKey('Trainee', related_name='level_qualifications', on_delete=models.CASCADE)
level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE)
trainee = models.ForeignKey('Trainee', related_name='levels', on_delete=models.RESTRICT)
level = models.ForeignKey('TrainingLevel', on_delete=models.RESTRICT)
confirmed_on = models.DateTimeField(null=True)
confirmed_by = models.ForeignKey('Trainee', related_name='confirmer', on_delete=models.CASCADE, null=True)
confirmed_by = models.ForeignKey('Trainee', related_name='confirmer', on_delete=models.RESTRICT, null=True)
reversion_hide = True
@property
def get_icon(self):
return self.level.get_icon
def clean(self):
if self.level.level >= TrainingLevel.SUPERVISOR and self.level.department != TrainingLevel.HAULAGE:
self.trainee.is_supervisor = True
self.trainee.save()
def __str__(self):
if self.level.is_common_competencies:
return f"{self.trainee} is qualified in the {self.level}"
return f"{self.trainee} is qualified as a {self.level}"
return "{} is qualified in the {}".format(self.trainee, self.level)
return "{} is qualified as a {}".format(self.trainee, self.level)
class Meta:
unique_together = ["trainee", "level"]
ordering = ['-confirmed_on']

View File

@@ -22,13 +22,6 @@
{% block content %}
{% if form.errors %}
{% include 'form_errors.html' %}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script>
//Has to be done here or the pickers disappear on modal error
$('document').ready(function(){
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
});
</script>
{% endif %}
<form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %}
{% render_field form.level|attr:'hidden' value=form.level.initial %}

View File

@@ -14,7 +14,7 @@
My Record
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMy">
<a class="dropdown-item" href="{% url 'trainee_detail' request.user.pk %}"><span class="fas fa-eye"></span>
<a class="dropdown-item" href="{% url 'trainee_detail' %}"><span class="fas fa-eye"></span>
Overview</a>
<a class="dropdown-item" href="{% url 'trainee_item_detail' request.user.pk %}"><span class="fas fa-list"></span>
Item Detail</a>

View File

@@ -13,27 +13,18 @@
{% endblock %}
{% block js %}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% endblock %}
{% block content %}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script>
//Has to be done here or the pickers disappear on modal error
$('document').ready(function(){
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
});
</script>
<form role="form" action="{{ form.action|default:request.path }}" method="POST" id="add_record_form">
{% include 'form_errors.html' %}
{% csrf_token %}
{% render_field form.trainee|attr:'hidden' value=form.trainee.initial %}
<div class="form-group form-row">
<label for="item_id" class="col-sm-2 col-form-label">Item</label>
<select name="item" id="item_id" class="form-control selectpicker custom-select col-sm-4" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=reference_number,name&filters=active" required>
{% if object.item %}
<option value="{{object.item.pk}}" selected>{{object.item}}</option>
{% endif %}
<select name="item" id="item_id" class="form-control selectpicker custom-select col-sm-4" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=reference_number,name" required>
</select>
</div>
<div class="form-group form-row">
@@ -41,26 +32,28 @@
{% render_field form.depth|add_class:'form-control custom-select col-sm-4' %}
</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&filters=is_supervisor" required>
{% if object.supervisor %}
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
{% endif %}
{% if external %}
<label for="supervisor" class="col-sm-2 col-form-label">Supervising Organisation</label>
<select name="supervisor" id="supervising_organisation_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" required>
</select>
{% else %}
<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>
{% endif %}
</div>
<div class="form-group form-row">
<label for="date" class="col-sm-2 col-form-label">Training Date</label>
<div class="col-sm-8">
{% with training_date=object.date|date:"Y-m-d" %}
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
{% endwith %}
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=form.date.initial %}
</div>
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#id_date').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
</div>
<div class="form-group form-row">
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
<div class="col-sm-8">
{% render_field form.notes|add_class:'form-control' rows=3 %}
<label for="item_description" class="col-sm-2 col-form-label">Notes</label>
<div class="col-sm-10">
<textarea type="text" placeholder="Notes" class="form-control"
id="notes" rows="3"></textarea>
</div>
</div>
{% if not request.is_ajax %}

View File

@@ -1,28 +1,18 @@
{% extends 'base_training.html' %}
{% block content %}
<div id="accordion">
{% for category in categories %}
<div class="card">
<div class="card-header" id="heading{{forloop.counter}}">
<button class="btn btn-link collapsed" data-toggle="collapse" data-target="#collapse{{forloop.counter}}" aria-expanded="true" aria-controls="collapse{{forloop.counter}}">
{{ category }}
</button>
</div>
<div id="collapse{{forloop.counter}}" class="collapse" aria-labelledby="heading{{forloop.counter}}" data-parent="#accordion">
<div class="card-body">
<div class="list-group list-group-flush">
{% for item in category.items.all %}
{% if item.active %}
<li class="list-group-item">{{ item }}</li>
{% elif request.user.is_superuser %}
<li class="list-group-item text-warning">{{ item }}</li>
{% endif %}
{% endfor %}
<div class="row">
{% for category in categories %}
<div class="col">
<div class="card mb-3">
<h4 class="card-header">{{ category.name }}</h4>
<div class="list-group list-group-flush">
{% for item in category.items.all %}
<li class="list-group-item">{{ item }}</li>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% endblock %}

View File

@@ -2,7 +2,6 @@
{% load user_has_qualification from tags %}
{% load user_level_if_present from tags %}
{% load markdown_tags %}
{% load static %}
{% block css %}
@@ -44,7 +43,7 @@
{% endblock %}
{% block content %}
{% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
{% if request.user.is_supervisor or perms.training.change_traininglevel %}
<div class="col-sm-12 text-right pr-0">
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
<span class="fas fa-plus"></span> Add New Requirement
@@ -54,48 +53,57 @@
<div class="card mb-3">
<h4 class="card-header">Description</h4>
<div class="card-body">
<p>{{ object.description|markdown }}</p>
<p>{{ object.description }}</p>
</div>
</div>
<div class="card mb-3 d-none d-md-block">
<h4 class="card-header">Users with this level</h4>
<div class="card-body">
<div class="card-columns" style="column-count: 5">
{% for user in users_with %}
{% user_level_if_present user object as level_qualification %}
<div class="card" {% if not level_qualification.confirmed_on %}style="border-style: dashed; opacity: 70%"{%endif%}>
<img src="{{user.profile_picture}}" class="card-img-top" />
<div class="card-body">
<h5 class="card-title">{{user}}</h5>
{% if level_qualification.confirmed_on %}<p class="card-text"><small>Qualified on {{ level_qualification.confirmed_on }}</small></p>{%else%}Unconfirmed{%endif%}
</div>
<div class="card-footer text-right pr-1"><a href="{% url 'profile_detail' user.pk %}" class="btn btn-primary btn-sm"><span class="fas fa-user"></span> View Profile</a></div>
</div>
{% empty %}
Nobody here but us chickens... <span class="fas fa-egg text-warning"></span>
{% endfor %}
</div>
</div>
</div>
<div class="card">
<div class="card-header"><h4 class="card-title">Level Requirements</h4> {% if u.pk != request.user.pk %}<h5 class="card-subtitle font-italic">for {{ u }}</h5>{% endif %}</div>
<div class="table-responsive card-body">
<table class="table">
<thead>
<tr>
<th scope="col" class="table-warning" style="width: 33%">Training Started</th>
<th scope="col" class="table-success" style="width: 33%">Training Complete</th>
<th scope="col" class="table-info" style="width: 33%">Passed Out</th>
</tr>
</thead>
<tbody class="table-body">
{% for level in object.prerequisite_levels.all %}
<tr data-toggle="collapse" data-target="#{{level.pk}}" style="cursor: pointer;"><th colspan="3" class="text-center font-italic" data-toggle="collapse" data-target="#level_{{level.pk}}">{{level}} (prerequisite)</th></tr>
<tr id="level_{{level.pk}}" class="collapse">
<td><ul class="list-unstyled">{% for req in level.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in level.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in level.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %}</li>{% endfor %}</ul></td>
</tr>
{% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr>
</tbody>
</table>
</div>
<h4 class="card-header">Level Requirements</h4>
<table class="table card-body">
<thead>
<tr>
<th scope="col" class="table-warning">Training Started</th>
<th scope="col" class="table-success">Training Complete</th>
<th scope="col" class="table-info">Passed Out</th>
</tr>
</thead>
<tbody>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 0 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a>{% endif %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 1 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a>{% endif %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 2 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr>
</tbody>
</table>
<h4 class="card-header">Prerequisite Levels:</h4>
<div class="card-body">
<ul>
{% for level in object.prerequisite_levels.all %}
{% user_level_if_present u level as level_qualification %}
<li><a href="{% url 'level_detail' level.pk u.pk %}">{{ level }}</a> <span class="fas {% if level_qualification %}text-success fa-check{% if level_qualification.confirmed_by is not None %}-double{% endif %}{% else %}fa-hourglass-start text-warning{%endif%}"></span></li>
{% user_level_if_present request.user level as level_qualification %}
<li><a href="{{level.get_absolute_url}}">{{ level }}</a> <span class="fas {% if level_qualification %}text-success fa-check{% if level_qualification.confirmed_by is not None %}-double{% endif %}{% else %}fa-hourglass-start text-warning{%endif%}"></span></li>
{% for nested_level in level.prerequisite_levels.all %}
{% user_level_if_present u nested_level as nested_level_qualification %}
{% user_level_if_present request.user nested_level as nested_level_qualification %}
<ul>
<li><a href="{% url 'level_detail' nested_level.pk u.pk %}">{{ nested_level }}</a> <span class="fas {% if nested_level_qualification %}text-success fa-check{% if nested_level_qualification.confirmed_by is not None %}-double{% endif %}{% else %}fa-hourglass-start text-warning{%endif%}"></span></li>
<li><a href="{{nested_level.get_absolute_url}}">{{ nested_level }}</a> <span class="fas {% if nested_level_qualification %}text-success fa-check{% if nested_level_qualification.confirmed_by is not None %}-double{% endif %}{% else %}fa-hourglass-start text-warning{%endif%}"></span></li>
</ul>
{% endfor %}
{% empty %}
@@ -104,36 +112,6 @@
</ul>
</div>
</div>
<div class="card mb-3 mt-2">
<h4 class="card-header">Users with this level</h4>
<div class="card-body">
{% for user in users_with %}
{% user_level_if_present user object as level_qualification %}
{% if forloop.first %}
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Person</th>
<th scope="col">Confirmed?</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% endif %}
<tr {% if not level_qualification.confirmed_on %}style="border-style: dashed; opacity: 80%"{%endif%}>
<td><a href="{{user.get_absolute_url}}"><img src="{{user.profile_picture}}" style="width: 50px" class="img-thumbnail"/> {{user}}</a></td>
<td>{% if level_qualification.confirmed_on %}<p class="card-text"><small>Qualified on {{ level_qualification.confirmed_on }}</small></p>{%else%}Unconfirmed{%endif%}</td>
<td><a href="{% url 'profile_detail' user.pk %}" class="btn btn-primary btn-sm"><span class="fas fa-user"></span> View Profile</a></div></td>
</tr>
{% if forloop.last %}
</tbody>
</table>
{% endif %}
{% empty %}
Nobody here but us chickens... <span class="fas fa-egg text-warning"></span>
{% endfor %}
</div>
</div>
<div class="row">
<div class="col text-right">
{% include 'partials/last_edited.html' with target="traininglevel_history" %}

View File

@@ -1,9 +1,23 @@
{% extends 'base_training.html' %}
{% load colour_from_level from tags %}
{% load markdown_tags %}
{% block css %}
<style>
.level-1::after {
content: "";
position: relative;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 50px;
margin-top: -15px;
margin-bottom: -15px;
background: black;
}
</style>
{% endblock %}
{% block content %}
{% if request.user.is_staff %}
<div class="alert alert-info" role="alert">
<p>Please Note:</p>
<ul>
@@ -13,17 +27,28 @@
</ul>
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
</div>
{% endif %}
{% for level in object_list %}
{% ifchanged level.department %}
{% if not forloop.first %}</div>{% endif %}
<div class="card-group">
{% endifchanged %}
<div class="card mb-2 border-{{level.department_colour}}">
<div class="card-body">
<h3 class="card-title"><a href="{{level.get_absolute_url}}">{{level}}</a></h2>
{{level.description|markdown}}
<div class="chart">
{% for level in levels %}
{% if forloop.counter == 3 %}
<div class="row">
<div class="col-6">
{% endif %}
{% if level.level == 2 and level.department is None %}
</div>
<div class="col-6">
{% endif %}
<div class="{% if forloop.first %}level-1 row{% elif forloop.counter == 2 %}level-2 row{%endif%}">
<div class="card my-3">
<h3 class="card-header"><a href="{% url 'level_detail' level.pk %}">{{ level }}</a></h3>
<div class="card-body bg-{% colour_from_level level %}">
<p>{{ level.description }}</p>
</div>
</div>
</div>
{% if forloop.last %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
{% endblock %}

View File

@@ -1,5 +0,0 @@
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
<a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record">
<span class="fas fa-plus"></span> Add New Training Record
</a>
{% endif %}

View File

@@ -1,93 +1,113 @@
{% extends 'base_training.html' %}
{% load static %}
{% load user_has_qualification from tags %}
{% load percentage_complete from tags %}
{% load confirm_button from tags %}
{% load markdown_tags %}
{% load user_level_if_present from tags %}
{% load colour_from_depth from tags %}
{% block css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/selects.js' %}"></script>
{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$('document').ready(function(){
$('#add_record').click(function (e) {
e.preventDefault();
var url = $(this).attr("href");
$.ajax({
url: url,
success: function(){
$link = $(this);
$('#add_record,#add_external').click(function (e) {
e.preventDefault();
var url = $(this).attr("href");
$.ajax({
url: url,
success: function(){
$link = $(this);
// Anti modal inception
if ($link.parents('#modal').length === 0) {
modaltarget = $link.data('target');
modalobject = "";
$('#modal').load(url, function (e) {
$('#modal').modal();
//$(".selectpicker").selectpicker().each(function(){initPicker($(this))});
$(".selectpicker").selectpicker().each(function(){initPicker($(this))});
});
}
}
});
});
});
});
</script>
</script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12 text-right">
<div class="btn-group">
{% include 'partials/add_qualification.html' %}
<a href="{% url 'trainee_item_detail' object.pk %}" class="btn btn-info"><span class="fas fa-info-circle"></span> View Detailed Record</a>
<a href="{% url 'profile_detail' object.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View User Profile</a>
</div>
<a type="button" class="btn btn-success" href="{% url 'edit_record' object.pk %}" id="add_record">
<span class="fas fa-plus"></span> Add New Training Record
</a>
<a href="{% url 'trainee_item_detail' object.pk %}" class="btn btn-info"><span class="fas fa-info-circle"></span> View Detailed Record</a><br/>
</div>
</div>
<div class="row mb-3">
<h2 class="col-12">Training Levels</h2>
<ul class="list-group col-12">
{% for qual in completed_levels %}
<li class="list-group-item">
{{ qual.level.get_icon }}
<a href="{% url 'level_detail' qual.level.pk %}">{{ qual.level }}</a>
Confirmed by <a href="{{ qual.confirmed_by.get_absolute_url }}">{{ qual.confirmed_by|default:'System' }}</a> on {{ qual.confirmed_on|date }}
</li>
{% empty %}
<div class="alert alert-warning mx-auto">No qualifications in any levels yet...did someone forget to fill out the paperwork?</div>
{% endfor %}
</ul>
{% for qual in completed_levels %}
{% if forloop.first %}<div class="card-columns">{% endif %}
<div class="card my-3 border-success">
<h3 class="card-header"><a href="{% url 'level_detail' qual.level.pk %}">{{ qual }}</a></h3>
<div class="card-footer text-right pr-1">
{% if qual.confirmed_by is None %}
{% if request.user.pk != object.pk and request.user.is_supervisor %}
<span class="badge badge-warning">Awaiting Confirmation</span> <a class="btn btn-info" href="{% url 'confirm_level' object.pk qual.level.pk %}">Confirm</a>
{% else %}
<button class="btn btn-warning" disabled>Awaiting Confirmation</button>
{% endif %}
{% else %}
<button class="btn btn-success active">Confirmed <small>by {{ qual.confirmed_by }}</small></button>
{% endif %}
</div>
</div>
{% if forloop.last %}</div>{%endif%}
{% empty %}
<div class="alert alert-warning">No qualifications in any levels yet...did someone forget to fill out the paperwork?</div>
{% endfor %}
<div class="card-columns">
{% for level in started_levels %}
{% percentage_complete level object as completion %}
<div class="card my-3 border-warning">
<h3 class="card-header"><a href="{% url 'level_detail' level.pk object.pk %}">{{ level }}</a></h3>
<h3 class="card-header"><a href="{% url 'level_detail' level.pk %}">{{ level }}</a></h3>
<div class="card-body">
{{ level.description|markdown }}
</div>
<div class="card-footer">
<div class="progress">
<p>{{ level.description|truncatewords:30 }}</p>
<div class="progress mb-2">
<div class="progress-bar progress-bar-striped" role="progressbar" style="width: {{completion}}%" aria-valuenow="{{completion}}" aria-valuemin="0" aria-valuemax="100">{{completion}}% complete</div>
</div>
{% if completion == 100 %}
<br>
{% confirm_button request.user object level as cb %}
{% if cb %}
<div class="d-flex justify-content-between">{{ cb }}</div>
{% else %}
<p class="font-italic pt-2 pb-0">Missing prerequisite level(s)</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row">
<h2 class="col-10">Training Items</h2>
<div class="alert alert-info col-12" role="alert"><h3>Key: <span class="badge badge-warning">Training Started</span> <span class="badge badge-success">Training Complete</span> <span class="badge badge-info">Passed Out</span></h3></div>
{% for category in categories %}
{% if forloop.first or forloop.counter|divisibleby:3 %}<div class="card-deck col-12">{% endif %}
<div class="card mb-3">
<h3 class="card-header">{{ category }}</h3>
<div class="list-group list-group-flush">
{% for q in object.qualifications_obtained.all %}
{% if q.item.category == category %}
<li class="list-group-item list-group-item-{% colour_from_depth q.depth %}">{{q.item}} ({{q.date}})</li>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% empty %}
<li class="list-group-item text-muted">None yet...</li>
{% endfor %}
</div>
</div>
{% if forloop.counter|add:"1"|divisibleby:3 %}</div>{% endif %}
{% endfor %}
</div>
</div>
<div class="row">
<div class="col text-right">

View File

@@ -7,8 +7,7 @@
{% load colour_from_depth from tags %}
{% block content %}
<p class="text-muted text-right">Search by supervisor name, item name or item ID</p>{% include 'partials/list_search.html' %}
<div class="row pt-2">
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm">
@@ -19,22 +18,16 @@
<th>Date</th>
<th>Supervisor</th>
<th>Notes</th>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<th></th>
{% endif %}
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr id="row_item" {% if request.user.is_superuser and not object.item.active %}class="text-warning"{%endif%}>
<tr id="row_item">
<th scope="row" class="align-middle" id="cell_name">{{ object.item }}</th>
<td class="table-{% colour_from_depth object.depth %}">{{ object.get_depth_display }}</td>
<td>{{ object.date }}</td>
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
<td>{{ object.supervisor }}</td>
<td>{{ object.notes }}</td>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
{% endif %}
</tr>
{% empty %}
<tr class="table-warning">
@@ -46,9 +39,4 @@
</div>
</div>
</div>
<div class="row">
<div class="col text-right">
{% include 'partials/last_edited.html' with target="trainee_history" object=trainee %}
</div>
</div>
{% endblock %}

View File

@@ -5,19 +5,9 @@
{% load paginator from filters %}
{% load linkornone from filters %}
{% load button from filters %}
{% load get_levels_of_depth from tags %}
{% block js %}
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %}
{% include 'partials/list_search.html' %}
<div class="row pt-2">
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped">
@@ -25,7 +15,6 @@
<tr>
<th scope="col">Name</th>
<th>Van Driver?</th>
<th>Technician?</th>
<th>Supervisor?</th>
<th>Qualification Count</th>
<th></th>
@@ -35,11 +24,10 @@
{% for object in object_list %}
<tr id="row_item">
<th scope="row" class="align-middle" id="cell_name"><a href="{% url 'trainee_detail' object.pk %}">{{ object.name }} {% if request.user.pk == object.pk %}<span class="fas fa-user text-success"></span>{%endif%}</a></th>
<td>{{ object.is_driver|yesno|title }}</td>
<td>{% for level in object|get_levels_of_depth:1 %}{% if forloop.first %}Yes {%endif%}{{ level.get_icon }}{%empty%}No{%endfor%}</td>
<td>{% for level in object|get_levels_of_depth:2 %}{% if forloop.first %}Yes {%endif%}{{ level.get_icon }}{%empty%}No{%endfor%}</td>
<td>{{ object.num_qualifications }} {% if forloop.first and page_obj.number is 1 %} <span class="fas fa-crown text-warning"></span>{% endif %}</td>
<td style="white-space: nowrap">
<td {% if object.is_driver %}class="table-success"{%endif%}>{{ object.is_driver|yesno|title }}</td>
<td {% if object.is_supervisor %}class="table-success"{%endif%}>{{ object.is_supervisor|yesno|title }}</td>
<td>{{ object.qualifications_obtained.all|length }}</td>
<td>
<a class="btn btn-info" href="{% url 'trainee_detail' pk=object.pk %}"><span class="fas fa-eye"></span> View Training Record</a>
<a href="{% url 'trainee_item_detail' pk=object.pk %}" class="btn btn-info"><span class="fas fa-info-circle"></span> View Detailed Record</a>
</td>
@@ -54,5 +42,4 @@
</div>
</div>
</div>
{% paginator %}
{% endblock %}

View File

@@ -3,47 +3,30 @@ from django import template
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
from django.utils.text import normalize_newlines
from django.urls import reverse
from training import models
register = template.Library()
@register.simple_tag
def user_has_qualification(user, item, depth):
if models.TrainingItem.user_has_qualification(item, user, depth):
return mark_safe("<span class='fas fa-check text-success' title='You have this requirement'></span>")
if models.TrainingItem.user_has_qualification(item, user, depth) is not None:
return mark_safe("<span class='fas fa-check text-success'></span>")
else:
return mark_safe("<span class='fas fa-hourglass-start text-warning' title='You do not yet have this requirement'></span>")
return mark_safe("<span class='fas fa-hourglass-start text-warning'></span>")
@register.simple_tag
def user_level_if_present(user, level):
return models.TrainingLevelQualification.objects.filter(trainee=user, level=level).first()
@register.simple_tag
def percentage_complete(level, user):
return level.percentage_complete(user)
@register.simple_tag
def colour_from_depth(depth):
return models.TrainingItemQualification.get_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=models.TrainingLevel.HAULAGE).select_related('level').filter(level__level=level)
@register.simple_tag
def confirm_button(user, trainee, level):
if level.user_has_requirements(trainee):
string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>"
if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'):
string += "<a class='btn btn-info' href='{}'>Confirm</a>".format(reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk}))
return mark_safe(string)
else:
return ""
def colour_from_level(level):
return level.get_department_colour()

View File

@@ -1,37 +0,0 @@
import pytest
from training import models
from RIGS.models import Profile
@pytest.fixture
def trainee(db):
trainee = Profile.objects.create(username="trainee", first_name="Train", last_name="EE",
initials="TRN",
email="trainee@example.com", is_active=True, is_approved=True)
yield trainee
trainee.delete()
@pytest.fixture
def supervisor(db):
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV",
email="supervisor@example.com", is_supervisor=True, is_active=True, is_approved=True)
yield supervisor
supervisor.delete()
@pytest.fixture
def training_item(db):
training_category = models.TrainingCategory.objects.create(reference_number=1, name="The Basics")
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, name="How Not To Die")
yield training_item
training_category.delete()
training_item.delete()
@pytest.fixture
def level(db):
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
yield level
level.delete()

View File

@@ -1,42 +0,0 @@
from django.urls import reverse
from pypom import Region
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from PyRIGS.tests import regions
from PyRIGS.tests.pages import BasePage, FormPage
class TraineeDetail(BasePage):
URL_TEMPLATE = 'training/trainee/{pk}'
_name_selector = (By.XPATH, '//h2')
@property
def page_name(self):
return self.find_element(*self._name_selector).text
class AddQualification(FormPage):
URL_TEMPLATE = 'training/trainee/{pk}/add_qualification/'
_item_selector = (By.XPATH, '//div[1]/form/div[1]/div')
_supervisor_selector = (By.XPATH, '//div[1]/form/div[3]/div')
form_items = {
'depth': (regions.SingleSelectPicker, (By.ID, 'id_depth')),
'date': (regions.DatePicker, (By.ID, 'id_date')),
'notes': (regions.TextBox, (By.ID, 'id_notes')),
}
@property
def item_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._item_selector))
@property
def supervisor_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
@property
def success(self):
return 'add' not in self.driver.current_url

View File

@@ -1,46 +0,0 @@
import datetime
import time
from django.utils import timezone
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal
from PyRIGS.tests.pages import animation_is_finished
from training import models
from training.tests import pages
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
# assert page.name in str(trainee)
page.depth = "Training Started"
page.date = date = datetime.date(1984, 1, 1)
page.notes = "A note"
time.sleep(2) # Slow down for javascript
page.item_selector.toggle()
assert page.item_selector.is_open
page.item_selector.search(training_item.name)
time.sleep(2) # Slow down for javascript
page.item_selector.set_option(training_item.name, True)
assert page.item_selector.options[0].selected
page.item_selector.toggle()
page.supervisor_selector.toggle()
assert page.supervisor_selector.is_open
page.supervisor_selector.search(supervisor.name[:-6])
time.sleep(2) # Slow down for javascript
assert page.supervisor_selector.options[0].selected
page.supervisor_selector.toggle()
page.submit()
assert page.success
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
assert qualification.supervisor.pk == supervisor.pk
assert qualification.date == date
assert qualification.notes == "A note"
assert qualification.depth == models.TrainingItemQualification.STARTED

Some files were not shown because too many files have changed in this diff Show More