Compare commits
98 Commits
imgbot
...
6b19d0e8b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
6b19d0e8b8
|
|||
|
3e8cfe4f11
|
|||
|
7a70270dfd
|
|||
|
5160eb7f78
|
|||
|
99e05d91bb
|
|||
|
f094ace862
|
|||
|
945fb393c0
|
|||
|
c6157d3e2b
|
|||
|
2767777d0e
|
|||
|
19e6585e26
|
|||
|
747575b968
|
|||
|
046d0e461d
|
|||
|
b73b8401b6
|
|||
|
3fe388af26
|
|||
|
22dc83d595
|
|||
|
70d4c42676
|
|||
|
0727a23236
|
|||
| f0b3a6daf3 | |||
| 3c5f6da363 | |||
| ee9be86465 | |||
| 5554edf977 | |||
| 14b73f6f50 | |||
| 732affa0b2 | |||
| 7c830ee7e5 | |||
| d47d00d79b | |||
| 3b5b3b84d4 | |||
| aa8be6a6d0 | |||
| 640362c203 | |||
| 71a8823ac2 | |||
| c6de3dc9e2 | |||
| 8696bf5d94 | |||
| 0d9bf89180 | |||
| f4f2fbdc03 | |||
| 67aaada9e8 | |||
| fcae39c93c | |||
| d1970edfb3 | |||
| ce5efff268 | |||
|
522837c64e
|
|||
| e78decdf92 | |||
| 84a3c9db24 | |||
|
280a1d9604
|
|||
|
a184bbfa26
|
|||
|
4416e5bfcb
|
|||
|
21276bcca0
|
|||
|
bc465d67e9
|
|||
|
a644735cd6
|
|||
|
10326f884f
|
|||
|
0a0c9f15af
|
|||
|
dbb9e3e530
|
|||
|
55558d1a4a
|
|||
| 081c33ebc8 | |||
| 75410db752 | |||
| 06c6b9a36e | |||
| 13b1cea28b | |||
|
cddb76bf7e
|
|||
|
f4f1fb66a2
|
|||
|
d80aeca01f
|
|||
|
45dfe2db51
|
|||
|
de5997b9da
|
|||
|
4a121964dc
|
|||
|
df5e4c8e0a
|
|||
|
3601c14ab7
|
|||
|
adde6496f5
|
|||
|
ad734d94b2
|
|||
|
7d3ada822d
|
|||
|
732af53fda
|
|||
|
4fb0529cc0
|
|||
|
aa23b1cd09
|
|||
|
0c4228da57
|
|||
|
246a52d19e
|
|||
|
8b10aaf700
|
|||
|
4d0d4f02aa
|
|||
|
af987c1ebb
|
|||
|
d406a911bb
|
|||
|
63c5a68933
|
|||
|
66f7f830db
|
|||
|
9590c2066d
|
|||
|
8b48b02ca7
|
|||
|
68e7ec2a0d
|
|||
|
5779ebdf7e
|
|||
| be648c20d5 | |||
| b6ef7c1d89 | |||
| 85f40b358a | |||
| 2698798035 | |||
| dbaab5cf8c | |||
| 0a9f82e480 | |||
| 54f2bd36bd | |||
| e836195fef | |||
| 68a424d62b | |||
| 5e15b8bb59 | |||
| d26c1b535e | |||
| dff5ac2308 | |||
| a3729fa930 | |||
| 458a734331 | |||
| b1646d556c | |||
| f8624d3b7a | |||
| f6836fdab6 | |||
| b3949f2903 |
18
Pipfile
@@ -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,15 +89,8 @@ pytest-django = "*"
|
||||
pluggy = "*"
|
||||
pytest-splinter = "*"
|
||||
pytest = "*"
|
||||
pytest-reverse = "*"
|
||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
|
||||
[dev-packages.pytest-xdist]
|
||||
extras = [ "psutil",]
|
||||
version = "*"
|
||||
|
||||
[dev-packages.PyPOM]
|
||||
extras = [ "splinter",]
|
||||
version = "*"
|
||||
|
||||
872
Pipfile.lock
generated
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -101,13 +101,11 @@ class SecureAPIRequest(generic.View):
|
||||
for field in fields:
|
||||
q = Q(**{field + "__icontains": part})
|
||||
qs.append(q)
|
||||
|
||||
for filter in filters:
|
||||
q = Q(**{field: True})
|
||||
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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -35,8 +35,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():
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -27,7 +27,6 @@ class Profile(AbstractUser):
|
||||
# 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
|
||||
|
||||
@@ -69,7 +68,7 @@ class Profile(AbstractUser):
|
||||
return self.name
|
||||
|
||||
|
||||
class RevisionMixin:
|
||||
class RevisionMixin(object):
|
||||
@property
|
||||
def is_first_version(self):
|
||||
versions = Version.objects.get_for_object(self)
|
||||
@@ -99,7 +98,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 +206,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):
|
||||
@@ -347,10 +346,10 @@ class Event(models.Model, RevisionMixin):
|
||||
if self.pk:
|
||||
if self.is_rig:
|
||||
return str("N%05d" % self.pk)
|
||||
|
||||
return self.pk
|
||||
|
||||
return "????"
|
||||
else:
|
||||
return self.pk
|
||||
else:
|
||||
return "????"
|
||||
|
||||
# Calculated values
|
||||
"""
|
||||
@@ -475,7 +474,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 +520,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 +542,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 +670,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?")
|
||||
|
||||
@@ -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
|
||||
@@ -167,8 +167,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 +210,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 +266,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 +283,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 +297,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 +319,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 +335,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 +345,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 +406,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'),
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 278 KiB |
BIN
RIGS/static/imgs/square_logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.4 MiB After Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 852 KiB After Width: | Height: | Size: 852 KiB |
@@ -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)):
|
||||
@@ -219,7 +221,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())
|
||||
|
||||
|
||||
@@ -284,11 +284,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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'assets.apps.AssetsAppConfig'
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')},
|
||||
),
|
||||
]
|
||||
@@ -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})
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -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(){
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
@@ -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')))
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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'),
|
||||
|
||||
150
assets/views.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
9691
package-lock.json
generated
@@ -6,6 +6,3 @@ from reversion.admin import 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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,7 +17,7 @@ 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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
@@ -144,13 +143,7 @@ class Command(BaseCommand):
|
||||
"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)
|
||||
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):
|
||||
@@ -193,13 +186,26 @@ 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, 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())
|
||||
|
||||
@@ -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()
|
||||
@@ -48,7 +48,6 @@ class Command(BaseCommand):
|
||||
|
||||
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
|
||||
@@ -60,7 +59,6 @@ class Command(BaseCommand):
|
||||
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))
|
||||
|
||||
@@ -126,7 +124,7 @@ class Command(BaseCommand):
|
||||
for child in root:
|
||||
depths = [("Training_Started", models.TrainingItemQualification.STARTED),
|
||||
("Training_Complete", models.TrainingItemQualification.COMPLETE),
|
||||
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
|
||||
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT),]
|
||||
|
||||
for (depth, depth_index) in depths:
|
||||
if child.find('{}_Date'.format(depth)) is not None:
|
||||
@@ -227,16 +225,17 @@ class Command(BaseCommand):
|
||||
|
||||
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))
|
||||
if child.find('Training_x0020_Level_x0020_ID') is None:
|
||||
print('Training Level Qualification #{} does not qualify in any level. How?'.format(child.find('ID').text))
|
||||
continue
|
||||
if child.find('Member_x0020_ID') is None:
|
||||
print('Training Level Qualification #{} does not qualify anyone. How?!'.format(child.find('ID').text))
|
||||
continue
|
||||
obj, created = models.TrainingLevelQualification.objects.update_or_create(
|
||||
pk=int(child.find('ID').text),
|
||||
trainee=Profile.objects.get(pk=self.id_map[child.find('Member_x0020_ID').text]),
|
||||
level=models.TrainingLevel.objects.get(pk=int(child.find('Training_x0020_Level_x0020_ID').text))
|
||||
)
|
||||
|
||||
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"))
|
||||
|
||||
@@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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']},
|
||||
),
|
||||
]
|
||||
42
training/migrations/0002_auto_20210706_0053.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
24
training/migrations/0003_auto_20210716_0150.py
Normal 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),
|
||||
),
|
||||
]
|
||||
21
training/migrations/0004_auto_20210819_1808.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
17
training/migrations/0005_auto_20210819_1833.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
23
training/migrations/0006_auto_20210903_2158.py
Normal 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')]),
|
||||
),
|
||||
]
|
||||
25
training/migrations/0007_auto_20210908_2043.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
training/migrations/0008_trainingitem_active.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-27 12:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0007_auto_20210908_2043'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trainingitem',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
22
training/migrations/0009_auto_20211221_1539.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.1.13 on 2021-12-21 15:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0008_trainingitem_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='trainingcategory',
|
||||
name='reference_number',
|
||||
field=models.CharField(max_length=3, unique=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='trainingitem',
|
||||
unique_together={('reference_number', 'active', 'category')},
|
||||
),
|
||||
]
|
||||
23
training/migrations/0010_auto_20211228_1144.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.13 on 2021-12-28 11:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0009_auto_20211221_1539'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='trainingcategory',
|
||||
name='reference_number',
|
||||
field=models.IntegerField(unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trainingitem',
|
||||
name='reference_number',
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
]
|
||||
23
training/migrations/0011_auto_20220102_1106.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.13 on 2022-01-02 11:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0010_auto_20211228_1144'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='traininglevelqualification',
|
||||
options={'ordering': ['-confirmed_on']},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='traininglevelqualification',
|
||||
name='trainee',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='level_qualifications', to='training.trainee'),
|
||||
),
|
||||
]
|
||||
23
training/migrations/0012_auto_20220102_2051.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.13 on 2022-01-02 20:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0011_auto_20220102_1106'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='traininglevel',
|
||||
name='department',
|
||||
field=models.IntegerField(blank=True, choices=[(0, 'Sound'), (1, 'Lighting'), (2, 'Power'), (3, 'Rigging'), (4, 'Haulage')], null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='traininglevel',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,11 +1,13 @@
|
||||
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
|
||||
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
|
||||
|
||||
@reversion.register(for_concrete_model=False, fields=[], follow=["qualifications_obtained", "level_qualifications"])
|
||||
@reversion.register(for_concrete_model=False)
|
||||
class Trainee(Profile, RevisionMixin):
|
||||
class Meta:
|
||||
proxy = True
|
||||
@@ -17,7 +19,15 @@ class Trainee(Profile, RevisionMixin):
|
||||
@property
|
||||
def is_technician(self):
|
||||
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
||||
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists()
|
||||
|
||||
|
||||
@property
|
||||
def is_supervisor(self):
|
||||
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||
.filter(level__level__gte=TrainingLevel.SUPERVISOR) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists()
|
||||
|
||||
@@ -34,35 +44,30 @@ class Trainee(Profile, RevisionMixin):
|
||||
def get_absolute_url(self):
|
||||
return reverse('trainee_detail', kwargs={'pk': self.pk})
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
class TrainingCategory(models.Model):
|
||||
reference_number = models.IntegerField(unique=True)
|
||||
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)
|
||||
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}"
|
||||
return "{}.{}".format(self.category.reference_number, self.reference_number)
|
||||
|
||||
def __str__(self):
|
||||
name = f"{self.display_id} {self.name}"
|
||||
name = "{} {}".format(self.display_id, self.name)
|
||||
if not self.active:
|
||||
name += " (inactive)"
|
||||
return name
|
||||
@@ -76,8 +81,8 @@ class TrainingItem(models.Model):
|
||||
ordering = ['category__reference_number', 'reference_number']
|
||||
|
||||
|
||||
@reversion.register
|
||||
class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
@reversion.register(follow=['trainee'])
|
||||
class TrainingItemQualification(models.Model):
|
||||
STARTED = 0
|
||||
COMPLETE = 1
|
||||
PASSED_OUT = 2
|
||||
@@ -86,12 +91,12 @@ 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)
|
||||
depth = models.IntegerField(choices=CHOICES)
|
||||
trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.CASCADE)
|
||||
trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.RESTRICT)
|
||||
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
|
||||
|
||||
@@ -103,13 +108,13 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
return str("{} in {}".format(self.get_depth_display(), self.item))
|
||||
|
||||
@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"
|
||||
else:
|
||||
return "info"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('trainee_item_detail', kwargs={'pk': self.trainee.pk})
|
||||
@@ -148,23 +153,20 @@ class TrainingLevel(models.Model, RevisionMixin):
|
||||
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:
|
||||
return "info"
|
||||
if self.department == self.LIGHTING:
|
||||
elif self.department == self.LIGHTING:
|
||||
return "dark"
|
||||
if self.department == self.POWER:
|
||||
elif self.department == self.POWER:
|
||||
return "danger"
|
||||
if self.department == self.RIGGING:
|
||||
elif self.department == self.RIGGING:
|
||||
return "warning"
|
||||
if self.department == self.HAULAGE:
|
||||
elif self.department == self.HAULAGE:
|
||||
return "light"
|
||||
|
||||
return "primary"
|
||||
else:
|
||||
return "primary"
|
||||
|
||||
def get_requirements_of_depth(self, depth):
|
||||
return self.requirements.filter(depth=depth)
|
||||
@@ -195,8 +197,8 @@ 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())
|
||||
@@ -223,7 +225,7 @@ class TrainingLevel(models.Model, RevisionMixin):
|
||||
@property
|
||||
def get_icon(self):
|
||||
if self.icon is not None:
|
||||
icon = f"<span class='fas fa-{self.icon}'></span>"
|
||||
icon = "<span class='fas fa-{}'></span>".format(self.icon)
|
||||
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))
|
||||
@@ -231,8 +233,8 @@ class TrainingLevel(models.Model, RevisionMixin):
|
||||
|
||||
@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
|
||||
@@ -244,12 +246,12 @@ class TrainingLevelRequirement(models.Model, RevisionMixin):
|
||||
unique_together = ["level", "item"]
|
||||
|
||||
|
||||
@reversion.register
|
||||
@reversion.register(follow=['trainee'])
|
||||
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='level_qualifications', 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
|
||||
|
||||
@@ -257,15 +259,10 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
|
||||
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"]
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
||||
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials&filters=is_supervisor" required>
|
||||
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required>
|
||||
{% if object.supervisor %}
|
||||
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
|
||||
{% endif %}
|
||||
|
||||
@@ -44,7 +44,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
|
||||
@@ -59,33 +59,31 @@
|
||||
</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>
|
||||
<table class="table card-body">
|
||||
<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.change_traininglevel %}<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.change_traininglevel %}<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.change_traininglevel %}<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>
|
||||
<h4 class="card-header">Prerequisite Levels:</h4>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% extends 'base_training.html' %}
|
||||
|
||||
{% load markdown_tags %}
|
||||
{% load get_supervisor from tags %}
|
||||
|
||||
{% block content %}
|
||||
{% if request.user.is_staff %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p>Please Note:</p>
|
||||
<ul>
|
||||
@@ -13,17 +13,10 @@
|
||||
</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>
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">{{level}}</div>
|
||||
<div class="card-body">{{level.description|markdown}}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
|
||||
{% if request.user.as_trainee.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>
|
||||
|
||||
@@ -43,11 +43,9 @@
|
||||
{% 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>
|
||||
{% 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>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
@@ -81,7 +79,7 @@
|
||||
{% 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>
|
||||
<p class="font-italic pt-2 pb-0">Missing prequisite level(s)</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
||||
<td>{{ object.notes }}</td>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
|
||||
<td>{% button 'edit' 'edit_qualification' object.pk %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -46,9 +46,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 %}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<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>{{ object.num_qualifications }} {% if forloop.first %} <span class="fas fa-crown text-warning"></span>{% endif %}</td>
|
||||
<td style="white-space: nowrap">
|
||||
<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>
|
||||
|
||||
@@ -33,6 +33,11 @@ def colour_from_depth(depth):
|
||||
return models.TrainingItemQualification.get_colour_from_depth(depth)
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_supervisor(tech):
|
||||
return models.TrainingLevel.objects.get(department=tech.department, level=models.TrainingLevel.SUPERVISOR)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,38 +1,5 @@
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
|
||||
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
|
||||
|
||||
from training import models
|
||||
|
||||
|
||||
def test_add_qualification(admin_client, trainee, admin_user):
|
||||
url = reverse('add_qualification', kwargs={'pk': trainee.pk})
|
||||
date = (timezone.now() + datetime.timedelta(days=3)).strftime("%Y-%m-%d")
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk})
|
||||
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
|
||||
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk})
|
||||
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
||||
|
||||
|
||||
def test_add_requirement(admin_client, level):
|
||||
url = reverse('add_requirement', kwargs={'pk': level.pk})
|
||||
response = admin_client.post(url)
|
||||
assertContains(response, level.pk)
|
||||
|
||||
|
||||
def test_trainee_detail(admin_client, trainee, admin_user):
|
||||
url = reverse('trainee_detail', kwargs={'pk': admin_user.pk})
|
||||
response = admin_client.get(url)
|
||||
assertContains(response, "Your Training Record")
|
||||
assertContains(response, "No qualifications in any levels")
|
||||
|
||||
url = reverse('trainee_detail', kwargs={'pk': trainee.pk})
|
||||
response = admin_client.get(url)
|
||||
assertNotContains(response, "Your")
|
||||
name = trainee.first_name + " " + trainee.last_name
|
||||
assertContains(response, f"{name}'s Training Record")
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from training.decorators import has_perm_or_supervisor
|
||||
from PyRIGS.decorators import permission_required_with_403
|
||||
|
||||
from training import views, models
|
||||
from versioning.views import VersionHistory
|
||||
@@ -10,12 +10,12 @@ urlpatterns = [
|
||||
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||
path('trainee/<int:pk>/',
|
||||
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||
name='trainee_detail'),
|
||||
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualification')(views.AddQualification.as_view()),
|
||||
path('trainee/<int:pk>/history', permission_required_with_403('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||
path('trainee/<int:pk>/add_qualification/', login_required(views.AddQualification.as_view()),
|
||||
name='add_qualification'),
|
||||
path('trainee/<int:pk>/edit_qualification/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()),
|
||||
path('trainee/<int:pk>/edit_qualification/', permission_required_with_403('training.change_trainingitemqualification')(views.EditQualification.as_view()),
|
||||
name='edit_qualification'),
|
||||
|
||||
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import reversion
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin
|
||||
from training import models, forms
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, Count
|
||||
from django.db.models import Q, Count, OuterRef, F, Subquery, Window
|
||||
|
||||
from PyRIGS.views import is_ajax, ModalURLMixin
|
||||
from training import models, forms
|
||||
from users import views
|
||||
|
||||
|
||||
@@ -16,7 +17,7 @@ class ItemList(generic.ListView):
|
||||
model = models.TrainingItem
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context = super(ItemList, self).get_context_data(**kwargs)
|
||||
context["page_title"] = "Training Items"
|
||||
context["categories"] = models.TrainingCategory.objects.all()
|
||||
return context
|
||||
@@ -30,7 +31,7 @@ class TraineeDetail(views.ProfileDetail):
|
||||
return self.model.objects.prefetch_related('qualifications_obtained')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context = super(TraineeDetail, self).get_context_data(**kwargs)
|
||||
if self.request.user.pk == self.object.pk:
|
||||
context["page_title"] = "Your Training Record"
|
||||
else:
|
||||
@@ -62,7 +63,6 @@ class TraineeItemDetail(generic.ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
trainee = models.Trainee.objects.get(pk=self.kwargs['pk'])
|
||||
context["trainee"] = models.Trainee.objects.get(pk=self.kwargs['pk'])
|
||||
context["page_title"] = "Detailed Training Record for <a href='{}'>{}</a>".format(trainee.get_absolute_url(), trainee)
|
||||
return context
|
||||
|
||||
@@ -97,17 +97,17 @@ class TraineeList(generic.ListView):
|
||||
def get_queryset(self):
|
||||
q = self.request.GET.get('q', "")
|
||||
|
||||
filt = Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(initials__icontains=q)
|
||||
filter = Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(initials__icontains=q)
|
||||
|
||||
# try and parse an int
|
||||
try:
|
||||
val = int(q)
|
||||
filt = filt | Q(pk=val)
|
||||
filter = filter | Q(pk=val)
|
||||
except: # noqa
|
||||
# not an integer
|
||||
pass
|
||||
|
||||
return self.model.objects.filter(filt).annotate(num_qualifications=Count('qualifications_obtained')).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item')
|
||||
return self.model.objects.filter(filter).annotate(num_qualifications=Count('qualifications_obtained')).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -120,14 +120,8 @@ class AddQualification(generic.CreateView, ModalURLMixin):
|
||||
model = models.TrainingItemQualification
|
||||
form_class = forms.QualificationForm
|
||||
|
||||
@transaction.atomic()
|
||||
@reversion.create_revision()
|
||||
def form_valid(self, form, *args, **kwargs):
|
||||
reversion.add_to_revision(form.cleaned_data['trainee'])
|
||||
return super().form_valid(form, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context = super(AddQualification, self).get_context_data(**kwargs)
|
||||
context["depths"] = models.TrainingItemQualification.CHOICES
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
@@ -151,22 +145,16 @@ class EditQualification(generic.UpdateView):
|
||||
form_class = forms.QualificationForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context = super(EditQualification, self).get_context_data(**kwargs)
|
||||
context["depths"] = models.TrainingItemQualification.CHOICES
|
||||
context['page_title'] = "Edit Qualification {} for {}".format(self.object, models.Trainee.objects.get(pk=self.kwargs['pk']))
|
||||
return context
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs = super(EditQualification, self).get_form_kwargs()
|
||||
kwargs['pk'] = self.kwargs['pk']
|
||||
return kwargs
|
||||
|
||||
@transaction.atomic()
|
||||
@reversion.create_revision()
|
||||
def form_valid(self, form, *args, **kwargs):
|
||||
reversion.add_to_revision(form.cleaned_data['trainee'])
|
||||
return super().form_valid(form, *args, **kwargs)
|
||||
|
||||
|
||||
class AddLevelRequirement(generic.CreateView, ModalURLMixin):
|
||||
template_name = "add_level_requirement.html"
|
||||
@@ -174,12 +162,12 @@ class AddLevelRequirement(generic.CreateView, ModalURLMixin):
|
||||
form_class = forms.RequirementForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context = super(AddLevelRequirement, self).get_context_data(**kwargs)
|
||||
context["page_title"] = "Add Requirements to Training Level {}".format(models.TrainingLevel.objects.get(pk=self.kwargs['pk']))
|
||||
return context
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs = super(AddLevelRequirement, self).get_form_kwargs()
|
||||
kwargs['pk'] = self.kwargs['pk']
|
||||
return kwargs
|
||||
|
||||
@@ -190,6 +178,7 @@ class AddLevelRequirement(generic.CreateView, ModalURLMixin):
|
||||
@reversion.create_revision()
|
||||
def form_valid(self, form, *args, **kwargs):
|
||||
reversion.add_to_revision(form.cleaned_data['level'])
|
||||
reversion.set_comment("Level requirement added")
|
||||
return super().form_valid(form, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -199,7 +188,7 @@ class RemoveRequirement(generic.DeleteView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = f"Delete Requirement '{self.object}' from Training Level {self.object.level}?"
|
||||
context["page_title"] = "Delete Requirement '{}' from Training Level {}?".format(self.object, self.object.level)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -216,13 +205,7 @@ class ConfirmLevel(generic.RedirectView):
|
||||
@transaction.atomic()
|
||||
@reversion.create_revision()
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
trainee = models.Trainee.objects.get(pk=kwargs['pk'])
|
||||
level_qualification, created = models.TrainingLevelQualification.objects.get_or_create(trainee=trainee, level=models.TrainingLevel.objects.get(pk=kwargs['level_pk']))
|
||||
|
||||
if created:
|
||||
level_qualification.confirmed_by = self.request.user
|
||||
level_qualification.confirmed_on = timezone.now()
|
||||
level_qualification.save()
|
||||
|
||||
reversion.add_to_revision(trainee)
|
||||
level_qualification = models.TrainingLevelQualification.objects.create(trainee=models.Trainee.objects.get(pk=kwargs['pk']), level=models.TrainingLevel.objects.get(pk=kwargs['level_pk']), confirmed_by=self.request.user, confirmed_on=timezone.now())
|
||||
reversion.add_to_revision(level_qualification.trainee)
|
||||
reversion.set_user(self.request.user)
|
||||
return reverse_lazy('trainee_detail', kwargs={'pk': kwargs['pk']})
|
||||
|
||||
@@ -85,32 +85,39 @@ class Command(BaseCommand):
|
||||
self.profiles.append(new_profile)
|
||||
|
||||
def setup_useful_profiles(self):
|
||||
super_user = models.Profile.objects.create_superuser(username="superuser",
|
||||
email="superuser@example.com", password="superuser", first_name="Super", last_name="User",
|
||||
initials="SU", is_active=True)
|
||||
super_user = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User",
|
||||
initials="SU",
|
||||
email="superuser@example.com", is_superuser=True, is_active=True,
|
||||
is_staff=True)
|
||||
super_user.set_password('superuser')
|
||||
super_user.save()
|
||||
|
||||
finance_user = models.Profile.objects.create_user(username="finance",
|
||||
email="financeuser@example.com", password="finance", first_name="Finance", last_name="User",
|
||||
initials="FU", is_active=True, is_approved=True)
|
||||
finance_user = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User",
|
||||
initials="FU",
|
||||
email="financeuser@example.com", is_active=True, is_approved=True)
|
||||
finance_user.groups.add(self.finance_group)
|
||||
finance_user.groups.add(self.keyholder_group)
|
||||
finance_user.set_password('finance')
|
||||
finance_user.save()
|
||||
|
||||
hs_user = models.Profile.objects.create_user(username="hs",
|
||||
email="hsuser@example.com", password="hs", first_name="HS", last_name="User",
|
||||
initials="HSU", is_active=True, is_approved=True)
|
||||
hs_user = models.Profile.objects.create(username="hs", first_name="HS", last_name="User",
|
||||
initials="HSU",
|
||||
email="hsuser@example.com", is_active=True, is_approved=True)
|
||||
hs_user.groups.add(self.hs_group)
|
||||
hs_user.groups.add(self.keyholder_group)
|
||||
hs_user.set_password('hs')
|
||||
hs_user.save()
|
||||
|
||||
keyholder_user = models.Profile.objects.create_user(username="keyholder",
|
||||
email="keyholderuser@example.com", password="keyholder", first_name="Keyholder", last_name="User",
|
||||
initials="KU", is_active=True,
|
||||
is_approved=True)
|
||||
keyholder_user = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User",
|
||||
initials="KU",
|
||||
email="keyholderuser@example.com", is_active=True,
|
||||
is_approved=True)
|
||||
keyholder_user.groups.add(self.keyholder_group)
|
||||
keyholder_user.set_password('keyholder')
|
||||
keyholder_user.save()
|
||||
|
||||
basic_user = models.Profile.objects.create_user(username="basic",
|
||||
email="basicuser@example.com", password="basic", first_name="Basic", last_name="User",
|
||||
initials="BU", is_active=True, is_approved=True)
|
||||
basic_user = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User",
|
||||
initials="BU",
|
||||
email="basicuser@example.com", is_active=True, is_approved=True)
|
||||
basic_user.set_password('basic')
|
||||
basic_user.save()
|
||||
|
||||
@@ -146,7 +146,7 @@ class RIGSVersionTestCase(TestCase):
|
||||
self.assertFalse(current_version.changes.fields_changed)
|
||||
self.assertTrue(current_version.changes.anything_changed)
|
||||
|
||||
self.assertIsNone(diffs[0].old)
|
||||
self.assertTrue(diffs[0].old is None)
|
||||
self.assertEqual(diffs[0].new.name, "TI I1")
|
||||
|
||||
# Edit the item
|
||||
@@ -188,4 +188,4 @@ class RIGSVersionTestCase(TestCase):
|
||||
self.assertTrue(current_version.changes.anything_changed)
|
||||
|
||||
self.assertEqual(diffs[0].old.name, "New Name")
|
||||
self.assertIsNone(diffs[0].new)
|
||||
self.assertTrue(diffs[0].new is None)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from diff_match_patch import diff_match_patch
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -5,52 +7,20 @@ from django.db.models import EmailField, IntegerField, TextField, CharField, Boo
|
||||
from django.utils.functional import cached_property
|
||||
from reversion.models import Version, VersionQuerySet
|
||||
|
||||
from RIGS import models
|
||||
from training.models import Trainee
|
||||
|
||||
class RevisionMixin:
|
||||
@property
|
||||
def is_first_version(self):
|
||||
versions = Version.objects.get_for_object(self)
|
||||
return len(versions) == 1
|
||||
|
||||
@property
|
||||
def current_version(self):
|
||||
version = Version.objects.get_for_object(self).select_related('revision').first()
|
||||
return version
|
||||
|
||||
@property
|
||||
def last_edited_at(self):
|
||||
version = self.current_version
|
||||
if version is None:
|
||||
return None
|
||||
return version.revision.date_created
|
||||
|
||||
@property
|
||||
def last_edited_by(self):
|
||||
version = self.current_version
|
||||
if version is None:
|
||||
return None
|
||||
return version.revision.user
|
||||
|
||||
@property
|
||||
def current_version_id(self):
|
||||
version = self.current_version
|
||||
if version is None:
|
||||
return None
|
||||
return "V{0} | R{1}".format(version.pk, version.revision.pk)
|
||||
|
||||
@property
|
||||
def date_created(self):
|
||||
return self.current_version.revision.date_created
|
||||
logger = logging.getLogger('tec.pyrigs')
|
||||
|
||||
|
||||
class FieldComparison:
|
||||
class FieldComparison(object):
|
||||
def __init__(self, field=None, old=None, new=None):
|
||||
self.field = field
|
||||
self._old = old
|
||||
self._new = new
|
||||
|
||||
def display_value(self, value):
|
||||
if isinstance(self.field, (IntegerField, CharField)) and self.field.choices is not None and len(self.field.choices) > 0:
|
||||
if (isinstance(self.field, IntegerField) or isinstance(self.field, CharField)) and self.field.choices is not None and len(self.field.choices) > 0:
|
||||
choice = [x[1] for x in self.field.choices if x[0] == value]
|
||||
if len(choice) > 0:
|
||||
return choice[0]
|
||||
@@ -101,8 +71,8 @@ class FieldComparison:
|
||||
return outputDiffs
|
||||
|
||||
|
||||
class ModelComparison:
|
||||
def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=['date_joined']):
|
||||
class ModelComparison(object):
|
||||
def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=[]):
|
||||
# recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects
|
||||
try:
|
||||
self.fields = old._meta.get_fields()
|
||||
@@ -147,13 +117,12 @@ class ModelComparison:
|
||||
|
||||
@cached_property
|
||||
def item_changes(self):
|
||||
from RIGS.models import EventAuthorisation
|
||||
if self.follow and self.version.object is not None:
|
||||
item_type = ContentType.objects.get_for_model(self.version.object)
|
||||
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type)
|
||||
new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(EventAuthorisation))
|
||||
new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(models.EventAuthorisation))
|
||||
|
||||
comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'date_joined']}
|
||||
comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'last_login']}
|
||||
|
||||
# Build some dicts of what we have
|
||||
item_dict = {} # build a list of items, key is the item_pk
|
||||
@@ -201,7 +170,7 @@ class RIGSVersionManager(VersionQuerySet):
|
||||
for model in model_array:
|
||||
content_types.append(ContentType.objects.get_for_model(model))
|
||||
|
||||
return self.filter(content_type__in=content_types).select_related("revision",).order_by(
|
||||
return self.filter(content_type__in=content_types).select_related("revision").order_by(
|
||||
"-revision__date_created")
|
||||
|
||||
|
||||
|
||||