diff --git a/RIGS/forms.py b/RIGS/forms.py index 9caf3016..18a81e14 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -39,6 +39,7 @@ class EmbeddedAuthenticationForm(AuthenticationForm): super().__init__(*args, **kwargs) self.fields['username'].widget.attrs.pop('autofocus', None) + class PasswordReset(PasswordResetForm): captcha = ReCaptchaField(label='Captcha') diff --git a/RIGS/management/commands/generateSampleData.py b/RIGS/management/commands/generateSampleData.py index 5263a030..3c82fe57 100644 --- a/RIGS/management/commands/generateSampleData.py +++ b/RIGS/management/commands/generateSampleData.py @@ -121,8 +121,19 @@ class Command(BaseCommand): self.keyholder_group = Group.objects.create(name='Keyholders') self.finance_group = Group.objects.create(name='Finance') - keyholderPerms = ["add_event", "change_event", "view_event", "add_eventitem", "change_eventitem", "delete_eventitem", "add_organisation", "change_organisation", "view_organisation", "add_person", "change_person", "view_person", "view_profile", "add_venue", "change_venue", "view_venue"] - financePerms = ["change_event", "view_event", "add_eventitem", "change_eventitem", "add_invoice", "change_invoice", "view_invoice", "add_organisation", "change_organisation", "view_organisation", "add_payment", "change_payment", "delete_payment", "add_person", "change_person", "view_person"] + keyholderPerms = ["add_event", "change_event", "view_event", + "add_eventitem", "change_eventitem", "delete_eventitem", + "add_organisation", "change_organisation", "view_organisation", + "add_person", "change_person", "view_person", "view_profile", + "add_venue", "change_venue", "view_venue", + "add_asset", "change_asset", "delete_asset", + "asset_finance"] + financePerms = ["change_event", "view_event", "add_eventitem", + "change_eventitem", "add_invoice", "change_invoice", "view_invoice", + "add_organisation", "change_organisation", "view_organisation", + "add_payment", "change_payment", "delete_payment", + "add_person", "change_person", "view_person", + "asset_finance", "change_asset"] for permId in keyholderPerms: self.keyholder_group.permissions.add(Permission.objects.get(codename=permId)) diff --git a/RIGS/migrations/0035_auto_20191008_2148.py b/RIGS/migrations/0035_auto_20191008_2148.py new file mode 100644 index 00000000..756ceac5 --- /dev/null +++ b/RIGS/migrations/0035_auto_20191008_2148.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.13 on 2019-10-08 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0034_event_risk_assessment_edit_url'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='risk_assessment_edit_url', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='risk assessment'), + ), + ] diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py index e6112b31..99c80601 100644 --- a/RIGS/rigboard.py +++ b/RIGS/rigboard.py @@ -83,6 +83,7 @@ class EventEmbed(EventDetail): class EventRA(generic.base.RedirectView): permanent = False + def get_redirect_url(self, *args, **kwargs): event = get_object_or_404(models.Event, pk=kwargs['pk']) @@ -406,6 +407,7 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView): context['to_name'] = self.request.GET.get('to_name', None) return context + @method_decorator(csrf_exempt, name='dispatch') class LogRiskAssessment(generic.View): http_method_names = ["post"] diff --git a/RIGS/views.py b/RIGS/views.py index aaaac7da..b197334c 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -62,7 +62,7 @@ def login_embed(request, **kwargs): messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.') request.method = 'GET' # Render the page without trying to login - return login(request, template_name="registration/login_embed.html", authentication_form=forms.EmbeddedAuthenticationForm) + return login(request, template_name="registration/login_embed.html", authentication_form=forms.EmbeddedAuthenticationForm) """ diff --git a/assets/admin.py b/assets/admin.py index bbd7b2d0..7044040f 100644 --- a/assets/admin.py +++ b/assets/admin.py @@ -32,11 +32,6 @@ class ConnectorAdmin(admin.ModelAdmin): list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins'] -@admin.register(assets.Cable) -class CableAdmin(admin.ModelAdmin): - pass - - admin.AdminSite.site_header = 'PyAssets - TEC\'s Asset System' admin.AdminSite.site_title = 'PyAssets Admin' admin.AdminSite.index_title = 'System Administration' diff --git a/assets/forms.py b/assets/forms.py index c727a9d1..19e00b11 100644 --- a/assets/forms.py +++ b/assets/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.core.exceptions import ValidationError from assets import models @@ -9,7 +10,7 @@ class AssetForm(forms.ModelForm): fields = '__all__' -class SupplierForm(forms.ModelForm): +class SupplierForm(forms.Form): class Meta: model = models.Supplier fields = '__all__' diff --git a/assets/management/commands/createSampleData.py b/assets/management/commands/createSampleData.py index 0d5beeb0..616cff7c 100644 --- a/assets/management/commands/createSampleData.py +++ b/assets/management/commands/createSampleData.py @@ -21,6 +21,7 @@ class Command(BaseCommand): self.create_statuses() self.create_suppliers() self.create_assets() + self.create_connectors() def create_categories(self): categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging'] @@ -35,7 +36,8 @@ class Command(BaseCommand): models.AssetStatus.objects.create(name=stat) def create_suppliers(self): - suppliers = ["Acme, inc.","Widget Corp","123 Warehousing","Demo Company","Smith and Co.","Foo Bars","ABC Telecom","Fake Brothers","QWERTY Logistics","Demo, inc.","Sample Company","Sample, inc","Acme Corp","Allied Biscuit","Ankh-Sto Associates","Extensive Enterprise","Galaxy Corp","Globo-Chem","Mr. Sparkle","Globex Corporation","LexCorp","LuthorCorp","North Central Positronics","Omni Consimer Products","Praxis Corporation","Sombra Corporation","Sto Plains Holdings","Tessier-Ashpool","Wayne Enterprises","Wentworth Industries","ZiffCorp","Bluth Company","Strickland Propane","Thatherton Fuels","Three Waters","Water and Power","Western Gas & Electric","Mammoth Pictures","Mooby Corp","Gringotts","Thrift Bank","Flowers By Irene","The Legitimate Businessmens Club","Osato Chemicals","Transworld Consortium","Universal Export","United Fried Chicken","Virtucon","Kumatsu Motors","Keedsler Motors","Powell Motors","Industrial Automation","Sirius Cybernetics Corporation","U.S. Robotics and Mechanical Men","Colonial Movers","Corellian Engineering Corporation","Incom Corporation","General Products","Leeding Engines Ltd.","Blammo","Input, Inc.","Mainway Toys","Videlectrix","Zevo Toys","Ajax","Axis Chemical Co.","Barrytron","Carrys Candles","Cogswell Cogs","Spacely Sprockets","General Forge and Foundry","Duff Brewing Company","Dunder Mifflin","General Services Corporation","Monarch Playing Card Co.","Krustyco","Initech","Roboto Industries","Primatech","Sonky Rubber Goods","St. Anky Beer","Stay Puft Corporation","Vandelay Industries","Wernham Hogg","Gadgetron","Burleigh and Stronginthearm","BLAND Corporation","Nordyne Defense Dynamics","Petrox Oil Company","Roxxon","McMahon and Tate","Sixty Second Avenue","Charles Townsend Agency","Spade and Archer","Megadodo Publications","Rouster and Sideways","C.H. Lavatory and Sons","Globo Gym American Corp","The New Firm","SpringShield","Compuglobalhypermeganet","Data Systems","Gizmonic Institute","Initrode","Taggart Transcontinental","Atlantic Northern","Niagular","Plow King","Big Kahuna Burger","Big T Burgers and Fries","Chez Quis","Chotchkies","The Frying Dutchman","Klimpys","The Krusty Krab","Monks Diner","Milliways","Minuteman Cafe","Taco Grande","Tip Top Cafe","Moes Tavern","Central Perk","Chasers"] + suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", + "Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] for supplier in suppliers: models.Supplier.objects.create(name=supplier) @@ -60,3 +62,14 @@ class Command(BaseCommand): asset.purchased_from = random.choice(suppliers) asset.save() + + def create_connectors(self): + connectors = [ + {"description": "13A UK", "current_rating": 13, "voltage_rating": 230, "num_pins": 3}, + {"description": "16A", "current_rating": 16, "voltage_rating": 230, "num_pins": 3}, + {"description": "32/3", "current_rating": 32, "voltage_rating": 400, "num_pins": 5}, + {"description": "Socapex", "current_rating": 23, "voltage_rating": 600, "num_pins": 19}, + ] + for connector in connectors: + conn = models.Connector.objects.create(**connector) + conn.save() diff --git a/assets/management/commands/deleteSampleData.py b/assets/management/commands/deleteSampleData.py index d58bee5f..f0338faa 100644 --- a/assets/management/commands/deleteSampleData.py +++ b/assets/management/commands/deleteSampleData.py @@ -21,7 +21,7 @@ class Command(BaseCommand): self.delete_objects(models.AssetCategory) self.delete_objects(models.AssetStatus) self.delete_objects(models.Supplier) - self.delete_objects(models.Collection) + self.delete_objects(models.Connector) self.delete_objects(models.Asset) def delete_objects(self, model): diff --git a/assets/migrations/0009_auto_20191008_2148.py b/assets/migrations/0009_auto_20191008_2148.py new file mode 100644 index 00000000..cbb474cc --- /dev/null +++ b/assets/migrations/0009_auto_20191008_2148.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.13 on 2019-10-08 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0008_auto_20191002_1931'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='asset_id', + field=models.CharField(max_length=10, unique=True), + ), + ] diff --git a/assets/migrations/0010_auto_20191013_2123.py b/assets/migrations/0010_auto_20191013_2123.py new file mode 100644 index 00000000..b56c40c4 --- /dev/null +++ b/assets/migrations/0010_auto_20191013_2123.py @@ -0,0 +1,67 @@ +# Generated by Django 2.0.13 on 2019-10-13 20:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0009_auto_20191008_2148'), + ] + + operations = [ + migrations.RemoveField( + model_name='cable', + name='asset_ptr', + ), + migrations.RemoveField( + model_name='cable', + name='plug', + ), + migrations.RemoveField( + model_name='cable', + name='socket', + ), + migrations.AlterModelOptions( + name='asset', + options={}, + ), + migrations.RemoveField( + model_name='asset', + name='polymorphic_ctype', + ), + migrations.AddField( + model_name='asset', + name='circuits', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='asset', + name='cores', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='asset', + name='csa', + field=models.DecimalField(blank=True, decimal_places=2, help_text='mm^2', max_digits=10, null=True), + ), + migrations.AddField( + model_name='asset', + name='length', + field=models.DecimalField(blank=True, decimal_places=1, help_text='m', max_digits=10, null=True), + ), + migrations.AddField( + model_name='asset', + name='plug', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'), + ), + migrations.AddField( + model_name='asset', + name='socket', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'), + ), + migrations.DeleteModel( + name='Cable', + ), + ] diff --git a/assets/migrations/0011_auto_20191013_2247.py b/assets/migrations/0011_auto_20191013_2247.py new file mode 100644 index 00000000..af49b124 --- /dev/null +++ b/assets/migrations/0011_auto_20191013_2247.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.13 on 2019-10-13 21:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0010_auto_20191013_2123'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='plug', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'), + ), + migrations.AlterField( + model_name='asset', + name='socket', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'), + ), + ] diff --git a/assets/migrations/0012_auto_20191014_0012.py b/assets/migrations/0012_auto_20191014_0012.py new file mode 100644 index 00000000..fdff40bb --- /dev/null +++ b/assets/migrations/0012_auto_20191014_0012.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.13 on 2019-10-13 23:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0011_auto_20191013_2247'), + ] + + operations = [ + migrations.AlterModelOptions( + name='asset', + options={'permissions': (('asset_finance', 'Can see financial data for assets'),)}, + ), + ] diff --git a/assets/models.py b/assets/models.py index 37a57628..005beadd 100644 --- a/assets/models.py +++ b/assets/models.py @@ -1,8 +1,10 @@ +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from polymorphic.models import PolymorphicModel import datetime +import re class AssetCategory(models.Model): @@ -37,10 +39,25 @@ class Supplier(models.Model): return self.name -class Asset(PolymorphicModel): +class Connector(models.Model): + description = models.CharField(max_length=80) + current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps') + voltage_rating = models.IntegerField(help_text='Volts') + num_pins = models.IntegerField(blank=True, null=True) + + def __str__(self): + return self.description + + +class Asset(models.Model): + class Meta: + ordering = ['asset_id'] + permissions = ( + ('asset_finance', 'Can see financial data for assets'), + ) parent = models.ForeignKey(to='self', related_name='asset_parent', blank=True, null=True, on_delete=models.SET_NULL) - asset_id = models.CharField(max_length=10) + asset_id = models.CharField(max_length=10, unique=True) description = models.CharField(max_length=120) category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE) status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE) @@ -55,34 +72,55 @@ class Asset(PolymorphicModel): # Cable assets is_cable = models.BooleanField(default=False) - - def get_absolute_url(self): - return reverse('asset_detail', kwargs={'pk': self.pk}) - - def __str__(self): - return str(self.asset_id) + ' - ' + self.description - -class Connector(models.Model): - description = models.CharField(max_length=80) - current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps') - voltage_rating = models.IntegerField(help_text='Volts') - num_pins = models.IntegerField(blank=True, null=True) - - def __str__(self): - return self.description - - -class Cable(Asset): - plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='plug', null=True) - socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='socket', null=True) + plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='plug', blank=True, null=True) + socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='socket', blank=True, null=True) length = models.DecimalField(decimal_places=1, max_digits=10, blank=True, null=True, help_text='m') csa = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text='mm^2') circuits = models.IntegerField(blank=True, null=True) cores = models.IntegerField(blank=True, null=True) - def cable_resistance(self): - rho = 0.0000000168 - return (rho * self.length) / (self.csa * 1000000) + def get_absolute_url(self): + return reverse('asset_detail', kwargs={'pk': self.pk}) def __str__(self): - return '{} - {}m - {}'.format(self.plug, self.length, self.socket) + out = str(self.asset_id) + ' - ' + self.description + if self.is_cable: + out += '{} - {}m - {}'.format(self.plug, self.length, self.socket) + return out + + def clean(self): + if self.date_sold and self.date_acquired > self.date_sold: + raise ValidationError({"date_sold": "Cannot sell an item before it is acquired"}) + + self.asset_id = self.asset_id.upper() + if re.search("^[a-zA-Z0-9]+$", self.asset_id) is None: + raise ValidationError({"asset_id": "An Asset ID can only consist of letters and numbers"}) + + if self.purchase_price and self.purchase_price < 0: + raise ValidationError({"purchase_price": "A price cannot be negative"}) + + if self.salvage_value and self.salvage_value < 0: + raise ValidationError({"purchase_price": "A price cannot be negative"}) + + if self.is_cable: + if self.length is None: + raise ValidationError({"length": "The length of a cable must be a number"}) + elif self.csa is None: + raise ValidationError({"csa": "The csa of a cable must be a number"}) + elif self.circuits is None: + raise ValidationError({"circuits": "The number of circuits in a cable must be a number"}) + elif self.cores is None: + raise ValidationError({"cores": "The number of cores in a cable must be a number"}) + elif self.socket is None: + raise ValidationError({"plug": "A cable must have a plug"}) + elif self.plug is None: + raise ValidationError({"socket": "A cable must have a socket"}) + + if self.length <= 0: + raise ValidationError({"length": "The length of a cable must be more than 0"}) + elif self.csa <= 0: + raise ValidationError({"csa": "The CSA of a cable must be more than 0"}) + elif self.circuits <= 0: + raise ValidationError({"circuits": "There must be at least one circuit in a cable"}) + elif self.cores <= 0: + raise ValidationError({"cores": "There must be at least one core in a cable"}) diff --git a/assets/templates/asset_create.html b/assets/templates/asset_create.html new file mode 100644 index 00000000..1e58c720 --- /dev/null +++ b/assets/templates/asset_create.html @@ -0,0 +1,71 @@ +{% extends 'base_assets.html' %} +{% load widget_tweaks %} +{% load asset_templatetags %} +{% block title %}Asset {{ object.asset_id }}{% endblock %} + + +{% block content %} + + +{% if duplicate %} +
+ {% else %} + + {% endif %} + {% include 'form_errors.html' %} +
+
+
+ {% include 'partials/asset_buttons.html' %} +
+
+
+ {% csrf_token %} + +
+
+ {% include 'partials/asset_form.html' %} +
+
+
+
+ {% include 'partials/purchasedetails_form.html' %} +
+ +
+ {% include 'partials/parent_form.html' %} +
+
+
+
+ {% include 'partials/asset_buttons.html' %} +
+
+
+ + {% include 'partials/confirm_delete.html' with object=object %} + + {% endblock %} + + {% block js%} + + {%endblock%} diff --git a/assets/templates/asset_list.html b/assets/templates/asset_list.html index a8a0da2a..c6aa1281 100644 --- a/assets/templates/asset_list.html +++ b/assets/templates/asset_list.html @@ -8,41 +8,48 @@

Asset List

-
+ {% csrf_token %} -
- +
+
- - -
- {% csrf_token %} -
- - -
-
- - -
- - +
+
+
+ + +
+
+ + +
+ + +
diff --git a/assets/templates/asset_update.html b/assets/templates/asset_update.html index 6c3de1e0..e4cbaa6f 100644 --- a/assets/templates/asset_update.html +++ b/assets/templates/asset_update.html @@ -9,304 +9,64 @@ - -
-
-
- {% include 'partials/asset_buttons.html' %} +
+ {% include 'form_errors.html' %} +
+
+
+ {% include 'partials/asset_buttons.html' %} +
-
- {% csrf_token %}
-
-
- Asset Details -
-
- {% if edit or duplicate %} -
- - {% if duplicate %} - {% render_field form.asset_id|add_class:'form-control' value=object.asset_id %} - {% elif object.asset_id %} - {% render_field form.asset_id|attr:'readonly disabled'|add_class:'disabled_input form-control' value=object.asset_id %} - {% else %} - {% render_field form.asset_id|add_class:'form-control' %} - {% endif %} -
-
- - {% render_field form.description|add_class:'form-control' value=object.description %} -
-
- - -
-
- - -
-
- - {% render_field form.serial_number|add_class:'form-control' value=object.serial_number %} -
- -
- - {% render_field form.comments|add_class:'form-control' %} -
- {% else %} -
Asset ID
-
{{ object.asset_id }}
- -
Description
-
{{ object.description }}
- -
Category
-
{{ object.category }}
- -
Status
-
{{ object.status }}
- -
Serial Number
-
{{ object.serial_number|default:'-' }}
- -
Comments
-
{{ object.comments|default:'-'|linebreaksbr }}
- {% endif %} -
-
+ {% include 'partials/asset_form.html' %}
+ {% if perms.asset.asset_financial %}
-
-
- Purchase Details -
-
- {% if edit or duplicate %} -
- - -
- -
- -
- £ - {% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %} -
-
- -
- -
- £ - {% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %} -
-
- -
- - {% if object.date_acquired %} - {% render_field form.date_acquired|add_class:'form-control'|attr:'type="date"' value=object.date_acquired|date %} - {% else %} - - {% endif %} -
- -
- - {% render_field form.date_sold|add_class:'form-control'|attr:'type="date"' value=object.date_sold|date %} -
- {% else %} -
-
Purchased From
-
{{ object.purchased_from|default_if_none:'-' }}
- -
Purchase Price
-
£{{ object.purchase_price|default_if_none:'-' }}
- -
Salvage Value
-
£{{ object.salvage_value|default_if_none:'-' }}
- -
Date Acquired
-
{{ object.date_acquired|default_if_none:'-' }}
- {% if object.date_sold %} -
Date Sold
-
{{ object.date_sold|default_if_none:'-' }}
- {% endif %} -
- {% endif %} -
-
-
-
-
-
- Collection Details -
-
- {% if edit or duplicate %} -
- - -
- - -
-
- -
- -
- - -
-
-
- -
-
-
- -
- {% else %} -
-
Parent
-
- {% if object.parent %} - - {{ object.parent.asset_id }} - {{ object.parent.description }} - - {% else %} - - - {% endif %} -
- -
Children
- {% if object.asset_parent.all %} - {% for child in object.asset_parent.all %} -
- - {{ child.asset_id }} - {{ child.description }} - -
- {% endfor %} - {% else %} -
-
- {% endif %} -
- {% endif %} -
-
-
-
- - -
-
- {% include 'partials/asset_buttons.html' %} + {% include 'partials/purchasedetails_form.html' %} +
+ {%endif%} + +
+ {% include 'partials/parent_form.html' %} +
-
+
+
+ {% include 'partials/asset_buttons.html' %} +
+
+ {% include 'partials/confirm_delete.html' with object=object %} {% endblock %} -{% block js %} +{% block js%} +{% if edit %} - -{# #} - - - - +{% endif %} {% endblock %} diff --git a/assets/templates/partials/asset_buttons.html b/assets/templates/partials/asset_buttons.html index d752e254..bf302351 100644 --- a/assets/templates/partials/asset_buttons.html +++ b/assets/templates/partials/asset_buttons.html @@ -1,20 +1,27 @@ +{% if edit and object %} + + + Duplicate + Delete +{% elif duplicate %} + + +{% elif create %} + + +{% else %} +
- {% if edit and object %} - - - Duplicate - Delete - {% elif duplicate %} - - - Cancel - {% elif not object %} - - - {% else %} - - Edit - Duplicate - Delete - {% endif %} + Edit + Duplicate + Delete
+{% endif %} +{% if create or edit or duplicate %} +
+ +{% endif %} diff --git a/assets/templates/partials/asset_form.html b/assets/templates/partials/asset_form.html new file mode 100644 index 00000000..86c2c7a4 --- /dev/null +++ b/assets/templates/partials/asset_form.html @@ -0,0 +1,75 @@ +{% load widget_tweaks %} +{% load asset_templatetags %} +
+
+ Asset Details +
+
+ {% if create or edit or duplicate %} +
+ + {% if duplicate %} + {% render_field form.asset_id|add_class:'form-control' value=object.asset_id %} + {% elif object.asset_id %} + {% render_field form.asset_id|attr:'readonly'|add_class:'disabled_input form-control' value=object.asset_id %} + {% else %} + {% render_field form.asset_id|add_class:'form-control' %} + {% endif %} +
+
+ + {% render_field form.description|add_class:'form-control' value=object.description %} +
+
+ + +
+ {% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} +
+ + +
+
+ + {% render_field form.serial_number|add_class:'form-control' value=object.serial_number %} +
+ +
+ + {% render_field form.comments|add_class:'form-control' %} +
+ {% else %} +
Asset ID
+
{{ object.asset_id }}
+ +
Description
+
{{ object.description }}
+ +
Category
+
{{ object.category }}
+ +
Status
+
{{ object.status }}
+ +
Serial Number
+
{{ object.serial_number|default:'-' }}
+ +
Comments
+
{{ object.comments|default:'-'|linebreaksbr }}
+ {% endif %} +
+
diff --git a/assets/templates/partials/asset_list_table_body.html b/assets/templates/partials/asset_list_table_body.html index 4972b993..17012240 100644 --- a/assets/templates/partials/asset_list_table_body.html +++ b/assets/templates/partials/asset_list_table_body.html @@ -1,7 +1,6 @@ {% for item in object_list %} {#
  • {{ item.asset_id }} - {{ item.description }}
  • #} - -
    - - - - + + + + diff --git a/assets/templates/partials/asset_picker.html b/assets/templates/partials/asset_picker.html new file mode 100644 index 00000000..444d95cd --- /dev/null +++ b/assets/templates/partials/asset_picker.html @@ -0,0 +1,63 @@ + + +{% load static %} +{% block css %} + + +{% endblock %} + +{% block preload_js %} + + +{% endblock %} + +{% block js %} + +{% endblock js %} \ No newline at end of file diff --git a/assets/templates/partials/cable_form.html b/assets/templates/partials/cable_form.html new file mode 100644 index 00000000..246f4ee9 --- /dev/null +++ b/assets/templates/partials/cable_form.html @@ -0,0 +1,75 @@ +{% load widget_tweaks %} +{% load asset_templatetags %} +
    +
    + Cable Details +
    +
    + {% if create or edit or duplicate %} +
    + + +
    +
    + + +
    +
    + +
    + {% render_field form.length|add_class:'form-control' %} + {{ form.length.help_text }} +
    +
    +
    + +
    + {% render_field form.csa|add_class:'form-control' value=object.csa %} + {{ form.csa.help_text }} +
    +
    +
    + + {% render_field form.circuits|add_class:'form-control' value=object.circuits %} +
    +
    + + {% render_field form.cores|add_class:'form-control' value=object.cores %} +
    + {% else %} +
    +
    Socket
    +
    {{ object.socket|default_if_none:'-' }}
    + +
    Plug
    +
    {{ object.plug|default_if_none:'-' }}
    + +
    Length
    +
    {{ object.length|default_if_none:'-' }}m
    + +
    Cross Sectional Area
    +
    {{ object.csa|default_if_none:'-' }}m^2
    + +
    Circuits
    +
    {{ object.circuits|default_if_none:'-' }}
    + +
    Cores
    +
    {{ object.cores|default_if_none:'-' }}
    +
    + {% endif %} +
    +
    diff --git a/assets/templates/partials/parent_form.html b/assets/templates/partials/parent_form.html new file mode 100644 index 00000000..10435818 --- /dev/null +++ b/assets/templates/partials/parent_form.html @@ -0,0 +1,41 @@ +{% load widget_tweaks %} +{% load asset_templatetags %} +
    +
    + Collection Details +
    +
    + {% if create or edit or duplicate %} +
    + + {% include 'partials/asset_picker.html' %} +
    + {% else %} +
    +
    Parent
    +
    + {% if object.parent %} + + {{ object.parent.asset_id }} - {{ object.parent.description }} + + {% else %} + - + {% endif %} +
    + +
    Children
    + {% if object.asset_parent.all %} + {% for child in object.asset_parent.all %} +
    + + {{ child.asset_id }} - {{ child.description }} + +
    + {% endfor %} + {% else %} +
    -
    + {% endif %} +
    + {% endif%} +
    +
    diff --git a/assets/templates/partials/purchasedetails_form.html b/assets/templates/partials/purchasedetails_form.html new file mode 100644 index 00000000..abfb2b26 --- /dev/null +++ b/assets/templates/partials/purchasedetails_form.html @@ -0,0 +1,76 @@ +{% load widget_tweaks %} +{% load asset_templatetags %} +
    +
    + Purchase Details +
    +
    + {% if create or edit or duplicate %} +
    + + +
    + +
    + +
    + £ + {% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %} +
    +
    + +
    + +
    + £ + {% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %} +
    +
    + +
    + + {% if object.date_acquired%} + {% with date_acq=object.date_acquired|date:"Y-m-d" %} + {% render_field form.date_acquired|add_class:'form-control'|attr:'type="date"' value=date_acq %} + {% endwith %} + {% else %} + + {% endif %} +
    + +
    + + {% with date_sol=object.form.date_sold|date:"Y-m-d" %} + {% render_field form.date_sold|add_class:'form-control'|attr:'type="date"' value=date_sol %} + {% endwith %} +
    + {% else %} +
    +
    Purchased From
    +
    {{ object.purchased_from|default_if_none:'-' }}
    + +
    Purchase Price
    +
    £{{ object.purchase_price|default_if_none:'-' }}
    + +
    Salvage Value
    +
    £{{ object.salvage_value|default_if_none:'-' }}
    + +
    Date Acquired
    +
    {{ object.date_acquired|default_if_none:'-' }}
    + {% if object.date_sold %} +
    Date Sold
    +
    {{ object.date_sold|default_if_none:'-' }}
    + {% endif %} +
    + {% endif %} +
    +
    diff --git a/assets/urls.py b/assets/urls.py index dcdf5569..c7dcf2fc 100644 --- a/assets/urls.py +++ b/assets/urls.py @@ -2,24 +2,26 @@ from django.urls import path, include from rest_framework import routers from assets import views, api +from PyRIGS.decorators import permission_required_with_403 + router = routers.DefaultRouter() router.register(r'api/assets', api.AssetViewSet) urlpatterns = [ - # path('', views.Index.as_view(), name='index'), path('', views.AssetList.as_view(), name='index'), path('asset/list/', views.AssetList.as_view(), name='asset_list'), path('asset//', views.AssetDetail.as_view(), name='asset_detail'), - path('asset/create/', views.AssetEdit.as_view(), name='asset_create'), - path('asset//edit/', views.AssetEdit.as_view(), name='asset_update'), - path('asset/delete/', views.asset_delete, name='ajax_asset_delete'), - path('asset/update/', views.asset_update, name='ajax_asset_update'), + path('asset/create/', permission_required_with_403('assets.create_asset')(views.AssetCreate.as_view()), name='asset_create'), + path('asset//edit/', permission_required_with_403('assets.change_asset')(views.AssetEdit.as_view()), name='asset_update'), + path('asset//duplicate/', permission_required_with_403('assets.create_asset')(views.AssetDuplicate.as_view()), name='asset_duplicate'), + path('asset/delete/', permission_required_with_403('assets.delete_asset')(views.asset_delete), name='ajax_asset_delete'), + + path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'), path('supplier/list', views.SupplierList.as_view(), name='supplier_list'), path('supplier/', views.SupplierDetail.as_view(), name='supplier_detail'), - path('supplier/create', views.SupplierCreate.as_view(), name='supplier_create'), - path('supplier//edit', views.SupplierUpdate.as_view(), name='supplier_update'), + path('supplier/create', permission_required_with_403('assets.create_supplier')(views.SupplierCreate.as_view()), name='supplier_create'), + path('supplier//edit', permission_required_with_403('assets.edit_supplier')(views.SupplierUpdate.as_view()), name='supplier_update'), path('', include(router.urls)), ] - diff --git a/assets/views.py b/assets/views.py index 62402a95..c706f949 100644 --- a/assets/views.py +++ b/assets/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpResponse, QueryDict +from django.http import HttpResponse, QueryDict, JsonResponse from django.core import serializers from django.views import generic from django.contrib.auth import views as auth_views @@ -13,103 +13,109 @@ from dateutil import parser import simplejson as json from assets import models, forms + class AssetList(LoginRequiredMixin, generic.ListView): model = models.Asset template_name = 'asset_list.html' paginate_by = 40 ordering = ['-pk'] - + def get_queryset(self): - #TODO Feedback to user when search fails + # TODO Feedback to user when search fails query = self.request.GET.get('query', "") - if len(query) >= 3: - return self.model.objects.filter(Q(asset_id__exact=query) | Q(description__icontains=query)) - elif query != "": - return self.model.objects.filter(Q(asset_id__exact=query)) + if len(query) == 0: + queryset = self.model.objects.all() + elif len(query) >= 3: + queryset = self.model.objects.filter(Q(asset_id__exact=query) | Q(description__icontains=query)) else: - cat = self.request.GET.get('cat', "") - status = self.request.GET.get('status', "") - if cat != "None": - return self.model.objects.filter(category__name__exact=cat) - elif status != "None": - return self.model.objects.filter(status__name__exact=status) - else: - return self.model.objects.all() - + queryset = self.model.objects.filter(Q(asset_id__exact=query)) + + cat = self.request.GET.get('cat', "") + status = self.request.GET.get('status', "") + if cat != "": + queryset = queryset.filter(category__name__exact=cat) + if status != "": + queryset = queryset.filter(status__name__exact=status) + + return queryset + def get_context_data(self, **kwargs): context = super(AssetList, self).get_context_data(**kwargs) context["search_name"] = self.request.GET.get('query', "") + context["categories"] = models.AssetCategory.objects.all() + context["category_select"] = self.request.GET.get('cat', "") + context["statuses"] = models.AssetStatus.objects.all() - return context; + context["status_select"] = self.request.GET.get('status', "") + return context + + +class AssetSearch(AssetList): + def render_to_response(self, context, **response_kwargs): + result = [] + + for asset in context["object_list"]: + result.append({"id": asset.pk, "label": (asset.asset_id + " | " + asset.description)}) + + return JsonResponse(result, safe=False) + class AssetDetail(LoginRequiredMixin, generic.DetailView): model = models.Asset template_name = 'asset_update.html' -# class AssetCreate(LoginRequiredMixin, generic.TemplateView): -# fields = '__all__' -# template_name = 'asset_update.html' -# # success_url = reverse_lazy('asset_list') - -class AssetEdit(LoginRequiredMixin, generic.TemplateView): +class AssetEdit(LoginRequiredMixin, generic.UpdateView): template_name = 'asset_update.html' + model = models.Asset + form_class = forms.AssetForm def get_context_data(self, **kwargs): - context = super(AssetEdit, self).get_context_data(**kwargs) - if self.kwargs: - context['object'] = get_object_or_404(models.Asset, pk=self.kwargs['pk']) - context['form'] = forms.AssetForm - # context['asset_names'] = models.Asset.objects.values_list('asset_id', 'description').order_by('-date_acquired')[] - - if self.request.GET.get('duplicate'): - context['duplicate'] = True - context['previous_asset_id'] = context['object'].asset_id - context['previous_asset_pk'] = context['object'].pk - context['object'].pk = 0 - context['object'].asset_id = '' - context['object'].serial_number = '' - else: - context['edit'] = True + context = super().get_context_data(**kwargs) + context['edit'] = True + context["connectors"] = models.Connector.objects.all() return context -@login_required() -def asset_update(request): - context = dict() + def get_success_url(self): + return reverse("asset_detail", kwargs={"pk": self.object.id}) - if request.method == 'POST' and request.is_ajax(): - defaults = QueryDict(request.POST['form'].encode('ASCII')).dict() - defaults.pop('csrfmiddlewaretoken') - asset_pk = int(defaults.pop('id')) +class AssetCreate(LoginRequiredMixin, generic.CreateView): + template_name = 'asset_create.html' + model = models.Asset + form_class = forms.AssetForm - if defaults['date_acquired']: - defaults['date_acquired'] = parser.parse(defaults.pop('date_acquired')) - else: - defaults['date_acquired'] = None + def get_context_data(self, **kwargs): + context = super(AssetCreate, self).get_context_data(**kwargs) - if defaults['date_sold']: - defaults['date_sold'] = parser.parse(defaults.pop('date_sold')) - else: - defaults['date_sold'] = None + context["create"] = True + context["connectors"] = models.Connector.objects.all() - # if defaults['parent']: - # defaults['parent'] = models.Asset.objects.get(asset_id=defaults.pop('parent')) + return context - form = forms.AssetForm(defaults) - context['valid'] = form.is_valid() - context['errors'] = form.errors.as_json() + def get_success_url(self): + return reverse("asset_detail", kwargs={"pk": self.object.id}) - if asset_pk == 0: - asset = models.Asset.objects.create(**form.cleaned_data) - else: - asset, created = models.Asset.objects.update_or_create(pk=asset_pk, defaults=form.cleaned_data) - context['url'] = reverse('asset_detail', args=[asset.pk]) +class DuplicateMixin: + def get(self, request, *args, **kwargs): + self.object = self.get_object() + self.object.pk = None + return self.render_to_response(self.get_context_data()) - return HttpResponse(json.dumps(context), content_type='application/json') + +class AssetDuplicate(DuplicateMixin, AssetCreate): + model = models.Asset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["create"] = None + context["duplicate"] = True + context['previous_asset_id'] = self.get_object().asset_id + context["previous_asset_pk"] = self.kwargs.get(self.pk_url_kwarg) + return context @login_required() @@ -123,6 +129,7 @@ def asset_delete(request): return HttpResponse(json.dumps(context), content_type='application/json') + class SupplierList(generic.ListView): model = models.Supplier template_name = 'supplier_list.html'
    {{ item.asset_id }}{{ item.description }}{{ item.category }}{{ item.status }}{{ item.asset_id }}{{ item.description }}{{ item.category }}{{ item.status }} +
    View + {% if perms.assets.change_asset %} Edit Duplicate + {% endif %}