Compare commits

..

21 Commits

Author SHA1 Message Date
Matthew Smith
1e00b23479 fix pep8 2020-01-17 12:38:02 +00:00
Matthew Smith
b5e61adde5 Disabled password reset and left message notifying user of problem. In response to CVE-2019-19844 2020-01-17 12:29:24 +00:00
4ad12ab40a FIX: Prevent basic users seeing individual asset version history
I prevented them from seeing the change stream, didn't prevent them seeing individual histories. This has to be done as otherwise it leaks financial information. If I can be arsed I'll come back to this and allow basic users to see a filtered version.
2020-01-11 21:13:50 +00:00
13205770f1 FIX: Correct template for AssetVersionHistory 2020-01-11 21:13:50 +00:00
6bb0c88c72 FIX: Migrations 2020-01-03 22:21:50 +00:00
82a30ca77d Miscellaneous changes to the Asset DB (#390)
* FIX #388: Prevent assets losing supplier data on edit

* FEAT: Add associated assets to supplier detail view

* FIX: Tweak supplier list to make detail view accessible

* Potential fix for #380

No idea if it works because I can't reproduce locally. S/O Reckons it should... :P

* FEAT #386: Asset search searches serial number.

Pending addition of advanced search.

* FIX: Order asset categories/statuses alphabetically

Instead of by pk because that's silly.

* FEAT: Statuses can have a CSS class defined in the admin panel

This replaces the hardcoding of colours in the asset list.

* FIX: Squash migrations

* Fixed supplier not working on all the create asset template

* Refactored away "assets" property on "Supplier" by using "related_name" instead

Co-authored-by: Matthew Smith <mattysmith22@googlemail.com>
2020-01-03 21:46:39 +00:00
David Taylor
97c0dffbd3 Order revisions by date created (#389) 2019-12-31 16:42:56 +00:00
David Taylor
3b28eafc82 Order RIGSVersions by date 2019-12-31 16:33:43 +00:00
ca8253894a FIX #321: Authorisation time shown as 'None' in emails (#378)
* FIX #321: Authorisation Success emails dated 'None'

* FIX: Additionally fix datestamp on HTML client emails (#321)
2019-12-31 12:45:38 +00:00
01a87e0e0b FEAT: Add revision history to assets and suppliers (#387)
* FEAT: Initial work on revision history for assets

The revision history for individual items mostly works, though it shows database ID where it should show asset ID. Recent changes feed isn't yet done.

* FEAT: Initial implementation of asset activity stream

* CHORE: Fix pep8

* FIX: Asset history table 'branding'

* FIX: Individual asset version history is now correctly filtered

* FEAT: Make revision history for suppliers accessible

* CHORE: *sings* And a pep8 in a broken tree...

* Refactored out duplicated code from `AssetVersionHistory

* CHORE: pep8

And another random bit of wierd whitespace I found

Co-authored-by: Matthew Smith <mattysmith22@googlemail.com>

Closes #358
2019-12-31 12:25:42 +00:00
Matthew Smith
7c876348d7 Asset fixes (#383) 2019-12-10 22:50:28 +00:00
ddc23ce4e5 FIX: Prefix field still too limited for legacy data
Fingers crossed this works I don't have the actual data locally... I know I'm making a mess but needs must.

I genuinely hate whoever decided prefixes were a good idea now.
2019-12-06 00:58:39 +00:00
602ccc15ea FIX: Fix missing import
Presumably caused by Matt's IDE being overzealous again. I know I shouldn't be pushing to master but...one line fix...
2019-12-06 00:40:56 +00:00
Matthew Smith
b77615b9b9 Fix handling of prefixed Asset IDs and sorting of the asset list (#382)
* FIX: Remove misleading admin site title

* Moved across assets_id sorting to use proper numeric values. Also mofifies SQL command to find free asset IDs so that it works on postgres.

* Changed generateSampleAssetsData.py to include prefices on some cables.

* Fixed pep8

* Fixed missed migration

* Ensured hidden asset fields are completed on every database write

* CMULTI is a thing, and therefore a max prefix length of 5 cannot be a thing
2019-12-06 00:28:54 +00:00
David Taylor
228d72b7b2 Automatically run migrations on deploy
Because running them via Heroku CLI is easy to forget
2019-12-05 17:26:02 +00:00
62541194ee CHORE: Fix pep8
mutter mutter mutter, grumble
2019-12-05 13:10:08 +00:00
0d8fd99d92 FIX: Permission errors
This fixes keyholders being unable to see financials information or create assets. (Well, the latter needs anyone to be able to create assets before it is fully fixed)
2019-12-05 13:00:47 +00:00
9d51a82f31 FIX: Fix asset sample data generation 2019-12-05 12:56:22 +00:00
c059227d5d Revert "CHANGE: Restrict viewing asset DB to keyholders."
This reverts commit 2c334196d5.
2019-12-05 12:42:05 +00:00
2c334196d5 CHANGE: Restrict viewing asset DB to keyholders.
This is in line with what it was when it was on the Shared Drive.
2019-12-04 23:59:39 +00:00
4f036af85a Create the Asset Database (#363) 2019-12-04 23:14:27 +00:00
30 changed files with 516 additions and 150 deletions

View File

@@ -1 +1,2 @@
release: python manage.py migrate
web: gunicorn PyRIGS.wsgi --log-file - web: gunicorn PyRIGS.wsgi --log-file -

View File

@@ -245,7 +245,7 @@
<div class="row"> <div class="row">
<div class="col-sm-10 align-left"> <div class="col-sm-10 align-left">
<a href="{% url 'event_history' object.pk %}" title="View Revision History"> <a href="{% url 'event_history' object.pk %}" title="View Revision History">
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }} Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
</a> </a>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">

View File

@@ -6,7 +6,7 @@
<p> <p>
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
for <b>&pound;{{ object.amount }}</b> for <b>&pound;{{ object.amount }}</b>
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>. by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>.
</p> </p>
<p> <p>

View File

@@ -1,6 +1,6 @@
Hi {{ to_name|default:"there" }}, Hi {{ to_name|default_if_none:"there" }},
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}. Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #} {% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Your event is now fully booked and payment will be processed by the finance department automatically. Your event is now fully booked and payment will be processed by the finance department automatically.

View File

@@ -1,5 +1,5 @@
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}}, Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}. Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
The TEC Rig Information Gathering System The TEC Rig Information Gathering System

View File

@@ -72,9 +72,8 @@
</div> </div>
</div> </div>
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
<div class="col-sm-6" > <div class="col-sm-6">
{% include 'RIGS/activity_feed.html' %} {% include 'RIGS/activity_feed.html' %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -0,0 +1,9 @@
{% extends 'base_rigs.html' %}
{% block title %}Password Reset Disabled{% endblock %}
{% block content %}
<h1>Password reset is disabled</h1>
<p> We are very sorry for the inconvenience, but due to a security vulnerability, password reset is currently disabled until the vulnerability can be patched.</p>
<p> If you are locked out of your account, please contact an administrator and we can manually perform a reset</p>
{% endblock %}

View File

@@ -19,7 +19,7 @@ urlpatterns = [
url('^user/login/$', views.login, name='login'), url('^user/login/$', views.login, name='login'),
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'), url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}), url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()),
# People # People
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()), url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),

View File

@@ -168,7 +168,7 @@ class RIGSVersionManager(VersionQuerySet):
for model in model_array: for model in model_array:
content_types.append(ContentType.objects.get_for_model(model)) content_types.append(ContentType.objects.get_for_model(model))
return self.filter(content_type__in=content_types).select_related("revision").order_by("-pk") return self.filter(content_type__in=content_types).select_related("revision").order_by("-revision__date_created")
class RIGSVersion(Version): class RIGSVersion(Version):
@@ -206,17 +206,14 @@ class VersionHistory(generic.ListView):
paginate_by = 25 paginate_by = 25
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
thisModel = self.kwargs['model'] return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", "revision__user").all().order_by("-revision__date_created")
versions = RIGSVersion.objects.get_for_object_reference(thisModel, self.kwargs['pk']).select_related("revision", "revision__user").all() def get_object(self, **kwargs):
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
return versions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
thisModel = self.kwargs['model']
context = super(VersionHistory, self).get_context_data(**kwargs) context = super(VersionHistory, self).get_context_data(**kwargs)
thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk']) context['object'] = self.get_object()
context['object'] = thisObject
return context return context
@@ -228,7 +225,7 @@ class ActivityTable(generic.ListView):
def get_queryset(self): def get_queryset(self):
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation]) versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
return versions return versions.order_by("-revision__date_created")
class ActivityFeed(generic.ListView): class ActivityFeed(generic.ListView):
@@ -238,7 +235,7 @@ class ActivityFeed(generic.ListView):
def get_queryset(self): def get_queryset(self):
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation]) versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
return versions return versions.order_by("-revision__date_created")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Call the base implementation first to get a context # Call the base implementation first to get a context

View File

@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
from RIGS import models, forms from RIGS import models, forms
from assets import models as asset_models
from functools import reduce from functools import reduce
""" """
@@ -248,6 +249,7 @@ class SecureAPIRequest(generic.View):
'organisation': models.Organisation, 'organisation': models.Organisation,
'profile': models.Profile, 'profile': models.Profile,
'event': models.Event, 'event': models.Event,
'supplier': asset_models.Supplier
} }
perms = { perms = {
@@ -256,6 +258,7 @@ class SecureAPIRequest(generic.View):
'organisation': 'RIGS.view_organisation', 'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile', 'profile': 'RIGS.view_profile',
'event': None, 'event': None,
'supplier': None
} }
''' '''
@@ -389,3 +392,7 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save() self.request.user.save()
return reverse_lazy('profile_detail') return reverse_lazy('profile_detail')
class PasswordResetDisabled(generic.TemplateView):
template_name = "RIGS/password_reset_disable.html"

View File

@@ -30,8 +30,3 @@ class AssetAdmin(admin.ModelAdmin):
@admin.register(assets.Connector) @admin.register(assets.Connector)
class ConnectorAdmin(admin.ModelAdmin): class ConnectorAdmin(admin.ModelAdmin):
list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins'] list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins']
admin.AdminSite.site_header = 'PyAssets - TEC\'s Asset System'
admin.AdminSite.site_title = 'PyAssets Admin'
admin.AdminSite.index_title = 'System Administration'

View File

@@ -6,4 +6,4 @@ from assets import models
class AssetFilter(django_filters.FilterSet): class AssetFilter(django_filters.FilterSet):
class Meta: class Meta:
model = models.Asset model = models.Asset
fields = ['asset_id', 'description', 'category', 'status'] fields = ['asset_id', 'description', 'serial_number', 'category', 'status']

View File

@@ -4,9 +4,15 @@ from assets import models
class AssetForm(forms.ModelForm): class AssetForm(forms.ModelForm):
related_models = {
'asset': models.Asset,
'supplier': models.Supplier
}
class Meta: class Meta:
model = models.Asset model = models.Asset
fields = '__all__' fields = '__all__'
exclude = ['asset_id_prefix', 'asset_id_number']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -1,6 +1,6 @@
import random
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone from django.utils import timezone
from assets import models from assets import models
@@ -49,7 +49,7 @@ class Command(BaseCommand):
suppliers = models.Supplier.objects.all() suppliers = models.Supplier.objects.all()
for i in range(100): for i in range(100):
asset = models.Asset.objects.create( asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()), asset_id='{}'.format(models.Asset.get_available_asset_id()),
description=random.choice(asset_description), description=random.choice(asset_description),
category=random.choice(categories), category=random.choice(categories),
@@ -62,11 +62,12 @@ class Command(BaseCommand):
if i % 3 == 0: if i % 3 == 0:
asset.purchased_from = random.choice(suppliers) asset.purchased_from = random.choice(suppliers)
asset.clean()
asset.save() asset.save()
def create_cables(self): def create_cables(self):
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep'] asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
csas = [0.75, 1.00, 1.25, 2.5, 4] csas = [0.75, 1.00, 1.25, 2.5, 4]
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100] lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
@@ -78,7 +79,7 @@ class Command(BaseCommand):
connectors = models.Connector.objects.all() connectors = models.Connector.objects.all()
for i in range(100): for i in range(100):
asset = models.Asset.objects.create( asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()), asset_id='{}'.format(models.Asset.get_available_asset_id()),
description=random.choice(asset_description), description=random.choice(asset_description),
category=random.choice(categories), category=random.choice(categories),
@@ -94,12 +95,17 @@ class Command(BaseCommand):
cores=random.choice(circuits) cores=random.choice(circuits)
) )
if i % 5 == 0:
prefix = random.choice(asset_prefixes)
asset.asset_id = prefix + str(models.Asset.get_available_asset_id(wanted_prefix=prefix))
if i % 4 == 0: if i % 4 == 0:
asset.parent = models.Asset.objects.order_by('?').first() asset.parent = models.Asset.objects.order_by('?').first()
if i % 3 == 0: if i % 3 == 0:
asset.purchased_from = random.choice(suppliers) asset.purchased_from = random.choice(suppliers)
asset.clean()
asset.save() asset.save()
def create_connectors(self): def create_connectors(self):

View File

@@ -0,0 +1,51 @@
# Generated by Django 2.0.13 on 2019-12-06 21:24
from django.db import migrations, models, transaction
import re
def forwards(apps, schema_editor):
AssetModel = apps.get_model('assets', 'Asset')
with transaction.atomic():
for row in AssetModel.objects.all():
row.asset_id = row.asset_id.upper()
asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id)
if asset_search is None: # If the asset_id doesn't have a number at the end
row.asset_id += "1"
asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id)
row.asset_id_prefix = asset_search.group(1)
row.asset_id_number = int(asset_search.group(2))
row.save(update_fields=['asset_id', 'asset_id_prefix', 'asset_id_number'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0007_auto_20190108_0202_squashed_0014_auto_20191017_2052'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': (('asset_finance', 'Can see financial data for assets'), ('view_asset', 'Can view an asset'))},
),
migrations.AddField(
model_name='asset',
name='asset_id_number',
field=models.IntegerField(default=1),
),
migrations.AddField(
model_name='asset',
name='asset_id_prefix',
field=models.CharField(default='', max_length=8),
),
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.CharField(max_length=15, unique=True),
),
migrations.RunPython(
code=forwards,
reverse_code=migrations.operations.special.RunPython.noop,
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 2.0.13 on 2020-01-03 22:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20191206_2124'),
]
operations = [
migrations.AlterModelOptions(
name='assetcategory',
options={'ordering': ['name'], 'verbose_name': 'Asset Category', 'verbose_name_plural': 'Asset Categories'},
),
migrations.AlterModelOptions(
name='assetstatus',
options={'ordering': ['name'], 'verbose_name': 'Asset Status', 'verbose_name_plural': 'Asset Statuses'},
),
migrations.AddField(
model_name='assetstatus',
name='display_class',
field=models.CharField(blank=True, help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80, null=True),
),
migrations.AlterField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Supplier'),
),
]

View File

@@ -3,11 +3,20 @@ from django.core.exceptions import ValidationError
from django.db import models, connection from django.db import models, connection
from django.urls import reverse from django.urls import reverse
from django.db.models.signals import pre_save
from django.dispatch.dispatcher import receiver
from reversion import revisions as reversion
from reversion.models import Version
from RIGS.models import RevisionMixin
class AssetCategory(models.Model): class AssetCategory(models.Model):
class Meta: class Meta:
verbose_name = 'Asset Category' verbose_name = 'Asset Category'
verbose_name_plural = 'Asset Categories' verbose_name_plural = 'Asset Categories'
ordering = ['name']
name = models.CharField(max_length=80) name = models.CharField(max_length=80)
@@ -19,15 +28,19 @@ class AssetStatus(models.Model):
class Meta: class Meta:
verbose_name = 'Asset Status' verbose_name = 'Asset Status'
verbose_name_plural = 'Asset Statuses' verbose_name_plural = 'Asset Statuses'
ordering = ['name']
name = models.CharField(max_length=80) name = models.CharField(max_length=80)
should_show = models.BooleanField(default=True, help_text="Should this be shown by default in the asset list.") should_show = models.BooleanField(
default=True, help_text="Should this be shown by default in the asset list.")
display_class = models.CharField(max_length=80, blank=True, null=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
def __str__(self): def __str__(self):
return self.name return self.name
class Supplier(models.Model): @reversion.register
class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80) name = models.CharField(max_length=80)
class Meta: class Meta:
@@ -52,21 +65,23 @@ class Connector(models.Model):
return self.description return self.description
class Asset(models.Model): @reversion.register
class Asset(models.Model, RevisionMixin):
class Meta: class Meta:
ordering = ['asset_id'] ordering = ['asset_id_prefix', 'asset_id_number']
permissions = ( permissions = (
('asset_finance', 'Can see financial data for assets'), ('asset_finance', 'Can see financial data for assets'),
('view_asset', 'Can view an asset') ('view_asset', 'Can view an asset')
) )
parent = models.ForeignKey(to='self', related_name='asset_parent', blank=True, null=True, on_delete=models.SET_NULL) parent = models.ForeignKey(to='self', related_name='asset_parent',
asset_id = models.CharField(max_length=10, unique=True) blank=True, null=True, on_delete=models.SET_NULL)
asset_id = models.CharField(max_length=15, unique=True)
description = models.CharField(max_length=120) description = models.CharField(max_length=120)
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE) category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE) status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
serial_number = models.CharField(max_length=150, blank=True) serial_number = models.CharField(max_length=150, blank=True)
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True) purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True, related_name="assets")
date_acquired = models.DateField() date_acquired = models.DateField()
date_sold = models.DateField(blank=True, null=True) date_sold = models.DateField(blank=True, null=True)
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10) purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
@@ -76,25 +91,35 @@ class Asset(models.Model):
# Cable assets # Cable assets
is_cable = models.BooleanField(default=False) is_cable = models.BooleanField(default=False)
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='plug', blank=True, null=True) plug = models.ForeignKey(Connector, on_delete=models.SET_NULL,
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='socket', blank=True, null=True) related_name='plug', blank=True, null=True)
length = models.DecimalField(decimal_places=1, max_digits=10, blank=True, null=True, help_text='m') socket = models.ForeignKey(Connector, on_delete=models.SET_NULL,
csa = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text='mm^2') 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) circuits = models.IntegerField(blank=True, null=True)
cores = models.IntegerField(blank=True, null=True) cores = models.IntegerField(blank=True, null=True)
def get_available_asset_id(): # Hidden asset_id components
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
asset_id_prefix = models.CharField(max_length=8, default="")
asset_id_number = models.IntegerField(default=1)
def get_available_asset_id(wanted_prefix=""):
sql = """ sql = """
SELECT MIN(CAST(a.asset_id AS int))+1 SELECT a.asset_id_number+1
FROM assets_asset a FROM assets_asset a
LEFT OUTER JOIN assets_asset b ON LEFT OUTER JOIN assets_asset b ON
(CAST(a.asset_id AS int) + 1 = CAST(b.asset_id AS int)) (a.asset_id_number + 1 = b.asset_id_number AND
WHERE b.asset_id IS NULL AND CAST(a.asset_id AS int) >= %s; a.asset_id_prefix = b.asset_id_prefix)
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
""" """
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(sql, [9000]) cursor.execute(sql, [9000, wanted_prefix])
row = cursor.fetchone() row = cursor.fetchone()
if row[0] is None: if row is None or row[0] is None:
return 9000 return 9000
else: else:
return row[0] return row[0]
@@ -114,8 +139,10 @@ class Asset(models.Model):
errdict["date_sold"] = ["Cannot sell an item before it is acquired"] errdict["date_sold"] = ["Cannot sell an item before it is acquired"]
self.asset_id = self.asset_id.upper() self.asset_id = self.asset_id.upper()
if re.search("^[a-zA-Z0-9]+$", self.asset_id) is None: asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id)
errdict["asset_id"] = ["An Asset ID can only consist of letters and numbers"] if asset_search is None:
errdict["asset_id"] = [
"An Asset ID can only consist of letters and numbers, with a final number"]
if self.purchase_price and self.purchase_price < 0: if self.purchase_price and self.purchase_price < 0:
errdict["purchase_price"] = ["A price cannot be negative"] errdict["purchase_price"] = ["A price cannot be negative"]
@@ -139,3 +166,14 @@ class Asset(models.Model):
if errdict != {}: # If there was an error when validation if errdict != {}: # If there was an error when validation
raise ValidationError(errdict) raise ValidationError(errdict)
@receiver(pre_save, sender=Asset)
def pre_save_asset(sender, instance, **kwargs):
"""Automatically fills in hidden members on database access"""
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
if asset_search is None:
instance.asset_id += "1"
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
instance.asset_id_prefix = asset_search.group(1)
instance.asset_id_number = int(asset_search.group(2))

View File

@@ -0,0 +1,92 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
{% load static %}
{% load paginator from filters %}
{% load to_class_name from filters %}
{% block title %}Asset Activity Stream{% endblock %}
{# TODO: Find a way to reduce code duplication...can't just include the content because of the IDs... #}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script src="{% static "js/popover.js" %}"></script>
<script src="{% static "js/moment.min.js" %}"></script>
<script>
$(function () {
$('[data-toggle="popover"]').popover().click(function(){
if($(this).attr('href')){
window.location.href = $(this).attr('href');
}
});
// This keeps timeago values correct, but uses an insane amount of resources
// $(function () {
// setInterval(function() {
// $('.date').each(function (index, dateElem) {
// var $dateElem = $(dateElem);
// var formatted = moment($dateElem.attr('data-date')).fromNow();
// $dateElem.text(formatted);
// })
// });
// }, 10000);
$('.date').each(function (index, dateElem) {
var $dateElem = $(dateElem);
var formatted = moment($dateElem.attr('data-date')).fromNow();
$dateElem.text(formatted);
});
})
</script>
{% endblock %}
{% block content %}
<div class="col-sm-12">
<div class="row">
<div class="col-sm-12">
<h3>Asset Activity Stream</h3>
</div>
<div class="text-right col-sm-12">{% paginator %}</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<td>Date</td>
<td>Object</td>
<td>Version ID</td>
<td>User</td>
<td>Changes</td>
<td>Comment</td>
</tr>
</thead>
<tbody>
{% for version in object_list %}
<tr>
<td>{{ version.revision.date_created }}</td>
<td><a href="{{ version.changes.new.get_absolute_url }}">{{version.changes.new|to_class_name}} {{ version.changes.new.asset_id|default:version.changes.new.pk }}</a></td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.changes.old == None %}
{{version.changes.new|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
{% endif %} </td>
<td>{{ version.changes.revision.comment }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="align-right">{% paginator %}</div>
</div>
{% endblock %}

View File

@@ -3,7 +3,6 @@
{% load asset_templatetags %} {% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %} {% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">

View File

@@ -11,8 +11,8 @@
<form id="asset-search-form" method="get" class="form-inline pull-right"> <form id="asset-search-form" method="get" class="form-inline pull-right">
<div class="input-group pull-right" style="width: auto;"> <div class="input-group pull-right" style="width: auto;">
{% render_field form.query|add_class:'form-control' placeholder='Search by Asset ID/Description' style="width: 250px"%} {% render_field form.query|add_class:'form-control' placeholder='Search by Asset ID/Desc/Serial' style="width: 250px"%}
<label for="query" class="sr-only">Asset ID/Description:</label> <label for="query" class="sr-only">Asset ID/Description/Serial Number:</label>
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span> <span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
</div> </div>
<br> <br>

View File

@@ -3,7 +3,6 @@
{% load asset_templatetags %} {% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %} {% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
@@ -25,7 +24,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
{% if perms.asset.asset_financial %} {% if perms.assets.asset_finance %}
<div class="col-md-6"> <div class="col-md-6">
{% include 'partials/purchasedetails_form.html' %} {% include 'partials/purchasedetails_form.html' %}
</div> </div>
@@ -45,6 +44,16 @@
</div> </div>
</form> </form>
{% if not edit and perms.assets.view_asset %}
<div class="col-sm-12 text-right">
<div>
<a href="{% url 'asset_history' object.asset_id %}" title="View Revision History">
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
</a>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block js%} {% block js%}

View File

@@ -0,0 +1,68 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
{% load to_class_name from filters %}
{% load paginator from filters %}
{% load static %}
{% block title %}{{object|to_class_name}} {{ object.asset_id }} - Revision History{% endblock %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script src="{% static "js/popover.js" %}"></script>
<script>
$(function () {
$('[data-toggle="popover"]').popover().click(function(){
if($(this).attr('href')){
window.location.href = $(this).attr('href');
}
});
})
</script>
{% endblock %}
{% block content %}
<div class="col-sm-12">
<div class="row">
<div class="col-sm-12">
<h3><a href="{{ object.get_absolute_url }}">{{object|to_class_name}} {{ object.asset_id|default:object.pk }}</a> - Revision History</h3>
</div>
<div class="text-right col-sm-12">{% paginator %}</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<td>Date</td>
<td>Version ID</td>
<td>User</td>
<td>Changes</td>
<td>Comment</td>
</tr>
</thead>
<tbody>
{% for version in object_list %}
<tr>
<td>{{ version.revision.date_created }}</td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.changes.old is None %}
{{object|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
{% endif %}
</td>
<td>
{{ version.revision.comment }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="align-right">{% paginator %}</div>
</div>
{% endblock %}

View File

@@ -1,26 +1,11 @@
{% for item in object_list %} {% for item in object_list %}
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#} {# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for--> <tr class=" <!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for--> <tr class={{ item.status.display_class|default:"" }}>
{% if item.status.name == 'Broken' %}
danger
{% elif item.status.name == 'Scrapped'%}
warning
{% elif item.status.name == 'Sold'%}
warning
{% elif item.status.name == 'Lost'%}
danger
{% elif item.status.name == 'Not Built Yet'%}
info
{% elif item.status.name == 'Active'%}
success
{% endif %}
">
<td style="vertical-align: middle;"><a href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td> <td style="vertical-align: middle;"><a href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
<td style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td> <td style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
<td style="vertical-align: middle;">{{ item.category }}</td> <td style="vertical-align: middle;">{{ item.category }}</td>
<td style="vertical-align: middle;">{{ item.status }}</td> <td style="vertical-align: middle;">{{ item.status }}</td>
<td class="hidden-xs"> <td class="hidden-xs">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.asset_id %}"><i class="glyphicon glyphicon-eye-open"></i> View</a> <a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.asset_id %}"><i class="glyphicon glyphicon-eye-open"></i> View</a>
{% if perms.assets.change_asset %} {% if perms.assets.change_asset %}

View File

@@ -1,5 +1,22 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load asset_templatetags %} {% load asset_templatetags %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static "js/bootstrap-select.js" %}"></script>
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
{% endblock %}
{% block js %}
<script src="{% static "js/autocompleter.js" %}"></script>
{% endblock %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
Purchase Details Purchase Details
@@ -7,8 +24,12 @@
<div class="panel-body"> <div class="panel-body">
{% if create or edit or duplicate %} {% if create or edit or duplicate %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.purchased_from.id_for_label }}">Purchased From</label> <label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
{% include 'partials/supplier_picker.html' %} <select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
{% if object.purchased_from %}
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
{% endif %}
</select>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@@ -1,64 +0,0 @@
<select name="purchased_from" id="supplier_id" class="selectpicker">
{% if object.parent%}
<option value="{{object.parent.pk}}" selected>{{object.parent.name}}</option>
{% endif %}
</select>
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static "js/bootstrap-select.js" %}"></script>
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
{% endblock %}
{% block js %}
{{ js.super }}
<script>
$('#supplier_id')
.selectpicker({
liveSearch: true
})
.ajaxSelectPicker({
ajax: {
url: '{% url 'supplier_search_json'%}',
type: "get",
data: function () {
var params = {
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
};
return params;
}
},
locale: {
emptyTitle: 'Search for supplier...'
},
preprocessData: function(data){
var suppliers = [];
if(data.length){
var len = data.length;
for(var i = 0; i < len; i++){
var curr = data[i];
suppliers.push(
{
'value': curr.id,
'text': curr.name,
'disabled': false
}
);
}
suppliers.push(
{
'value': null,
'text': "(no selection)"
});
}
return suppliers;
},
preserveSelected: false
});
</script>
{% endblock js %}

View File

@@ -1,6 +1,73 @@
{% extends 'base_assets.html' %} {% extends 'base_assets.html' %}
{% block title %}Detail{% endblock %} {% block title %}Supplier | {{ object.name }}{% endblock %}
{% block content %} {% block content %}
{{ object }} <div class="row">
{% if not request.is_ajax %}
<div class="col-sm-12">
<h1>Supplier | {{ object.name }}</h1>
</div>
<div class="col-sm-12 text-right">
<div class="btn-group btn-page">
<a href="{% url 'supplier_update' object.pk %}" class="btn btn-default"><span
class="glyphicon glyphicon-pencil"></span> Edit</a>
</div>
</div>
{% endif %}
<div class="col-sm-6">
<div class="panel panel-info">
<div class="panel-heading">Supplier Details</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>Name</dt>
<dd>{{ object.name }}</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">Associated Assets</div>
<div class="panel-body">
<table class="table">
<thead>
<tr>
<th>Asset ID</th>
<th>Description</th>
<th>Category</th>
<th>Status</th>
<th class="hidden-xs">Quick Links</th>
</tr>
</thead>
<tbody id="asset_table_body">
{% with object.assets.all as object_list %}
{% include 'partials/asset_list_table_body.html' %}
{% endwith %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% if not request.is_ajax %}
<div class="row">
<div class="col-sm-12 text-right">
<div class="btn-group btn-page">
<a href="{% url 'supplier_update' object.pk %}" class="btn btn-default"><span
class="glyphicon glyphicon-pencil"></span> Edit</a>
</div>
<div>
<a href="{% url 'supplier_update' object.pk %}" title="View Revision History">
Last edited {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
</a>
</div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -30,6 +30,7 @@
<tr> <tr>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td> <td>
<a href="{% url 'supplier_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
<a href="{% url 'supplier_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a> <a href="{% url 'supplier_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
</td> </td>
</tr> </tr>

View File

@@ -1,5 +1,7 @@
from django.conf.urls import url
from django.urls import path from django.urls import path
from assets import views from assets import views, models
from RIGS import versioning
from PyRIGS.decorators import permission_required_with_403 from PyRIGS.decorators import permission_required_with_403
@@ -7,16 +9,27 @@ urlpatterns = [
path('', views.AssetList.as_view(), name='asset_index'), path('', views.AssetList.as_view(), name='asset_index'),
path('asset/list/', views.AssetList.as_view(), name='asset_list'), path('asset/list/', views.AssetList.as_view(), name='asset_list'),
path('asset/id/<str:pk>/', views.AssetDetail.as_view(), name='asset_detail'), path('asset/id/<str:pk>/', views.AssetDetail.as_view(), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.create_asset')(views.AssetCreate.as_view()), name='asset_create'), path('asset/create/', permission_required_with_403('assets.add_asset')
path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')(views.AssetEdit.as_view()), name='asset_update'), (views.AssetCreate.as_view()), name='asset_create'),
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.create_asset')(views.AssetDuplicate.as_view()), name='asset_duplicate'), path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')
(views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<str:pk>/history/', permission_required_with_403('assets.view_asset')(views.AssetVersionHistory.as_view()),
name='asset_history', kwargs={'model': models.Asset}),
path('activity', permission_required_with_403('assets.view_asset')
(views.ActivityTable.as_view()), name='asset_activity_table'),
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'), path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'), path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'), path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
path('supplier/create', permission_required_with_403('assets.create_supplier')(views.SupplierCreate.as_view()), name='supplier_create'), path('supplier/create', permission_required_with_403('assets.add_supplier')
path('supplier/<int:pk>/edit', permission_required_with_403('assets.edit_supplier')(views.SupplierUpdate.as_view()), name='supplier_update'), (views.SupplierCreate.as_view()), name='supplier_create'),
path('supplier/<int:pk>/edit', permission_required_with_403('assets.change_supplier')
(views.SupplierUpdate.as_view()), name='supplier_update'),
path('supplier/<str:pk>/history/', views.SupplierVersionHistory.as_view(),
name='supplier_history', kwargs={'model': models.Supplier}),
path('supplier/search/', views.SupplierSearch.as_view(), name='supplier_search_json'), path('supplier/search/', views.SupplierSearch.as_view(), name='supplier_search_json'),
] ]

View File

@@ -5,7 +5,9 @@ from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.urls import reverse from django.urls import reverse
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404
from assets import models, forms from assets import models, forms
from RIGS import versioning
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
@@ -36,7 +38,8 @@ class AssetList(LoginRequiredMixin, generic.ListView):
if len(query_string) == 0: if len(query_string) == 0:
queryset = self.model.objects.all() queryset = self.model.objects.all()
elif len(query_string) >= 3: elif len(query_string) >= 3:
queryset = self.model.objects.filter(Q(asset_id__exact=query_string) | Q(description__icontains=query_string)) queryset = self.model.objects.filter(
Q(asset_id__exact=query_string) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
else: else:
queryset = self.model.objects.filter(Q(asset_id__exact=query_string)) queryset = self.model.objects.filter(Q(asset_id__exact=query_string))
@@ -46,7 +49,8 @@ class AssetList(LoginRequiredMixin, generic.ListView):
if len(form.cleaned_data['status']) > 0: if len(form.cleaned_data['status']) > 0:
queryset = queryset.filter(status__in=form.cleaned_data['status']) queryset = queryset.filter(status__in=form.cleaned_data['status'])
elif self.hide_hidden_status: elif self.hide_hidden_status:
queryset = queryset.filter(status__in=models.AssetStatus.objects.filter(should_show=True)) queryset = queryset.filter(
status__in=models.AssetStatus.objects.filter(should_show=True))
return queryset return queryset
@@ -203,3 +207,25 @@ class SupplierUpdate(generic.UpdateView):
model = models.Supplier model = models.Supplier
form_class = forms.SupplierForm form_class = forms.SupplierForm
template_name = 'supplier_update.html' template_name = 'supplier_update.html'
class SupplierVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html"
class AssetVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html"
def get_object(self, **kwargs):
return get_object_or_404(models.Asset, asset_id=self.kwargs['pk'])
class ActivityTable(versioning.ActivityTable):
model = versioning.RIGSVersion
template_name = "asset_activity_table.html"
paginate_by = 25
def get_queryset(self):
versions = versioning.RIGSVersion.objects.get_for_multiple_models(
[models.Asset, models.Supplier])
return versions

View File

@@ -1,11 +1,16 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block extrahead %}
<meta name="google" content="notranslate">
{% endblock %}
{% block titleheader %} {% block titleheader %}
<a class="nav navbar-brand navbar-left" href="/"><i class="glyphicon glyphicon-circle-arrow-left" style="vertical-align: middle !important;"></i> RIGS</a> <a class="nav navbar-brand navbar-left" href="/"><i class="glyphicon glyphicon-circle-arrow-left" style="vertical-align: middle !important;"></i> RIGS</a>
<a class="nav navbar-brand" href="{% url 'asset_index' %}">Assets</a> <a class="nav navbar-brand" href="{% url 'asset_index' %}">Assets</a>
{% endblock %} {% endblock %}
{% block titleelements %} {% block titleelements %}
{% if perms.assets.view_asset%} {# % if perms.assets.view_asset % #}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@@ -15,17 +20,20 @@
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{% endif %} {# % endif % #}
{% if perms.assets.view_supplier%} {# % if perms.assets.view_supplier % #}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span> <li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
List Suppliers</a></li> List Suppliers</a></li>
{% if perms.assets.add_asset %} {% if perms.assets.add_supplier %}
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li> <li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{# % endif % #}
{% if perms.assets.view_asset %}
<li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}