Compare commits

...

5 Commits

Author SHA1 Message Date
081a9eadfe Merge branch 'master' into auth_fixes 2019-12-31 12:34:08 +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
8e27dae895 FIX: Additionally fix datestamp on HTML client emails (#321) 2019-11-26 20:10:20 +00:00
22ef6b8e3a FIX #321: Authorisation Success emails dated 'None' 2019-11-26 18:18:49 +00:00
16 changed files with 306 additions and 93 deletions

View File

@@ -245,7 +245,7 @@
<div class="row">
<div class="col-sm-10 align-left">
<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>
</div>
<div class="col-sm-2">

View File

@@ -6,7 +6,7 @@
<p>
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
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>

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 #}
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"}},
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

View File

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

View File

@@ -206,17 +206,14 @@ class VersionHistory(generic.ListView):
paginate_by = 25
def get_queryset(self, **kwargs):
thisModel = self.kwargs['model']
return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", "revision__user").all()
versions = RIGSVersion.objects.get_for_object_reference(thisModel, self.kwargs['pk']).select_related("revision", "revision__user").all()
return versions
def get_object(self, **kwargs):
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
def get_context_data(self, **kwargs):
thisModel = self.kwargs['model']
context = super(VersionHistory, self).get_context_data(**kwargs)
thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk'])
context['object'] = thisObject
context['object'] = self.get_object()
return context

View File

@@ -1,60 +0,0 @@
# Generated by Django 2.0.13 on 2019-12-05 20:42
import re
from django.db import migrations, models
import django.db.migrations.operations.special
def forwards(apps, schema_editor):
AssetModel = apps.get_model('assets', 'Asset')
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'])
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# assets.migrations.0008_auto_20191205_1937
class Migration(migrations.Migration):
replaces = [('assets', '0008_auto_20191205_1937'), ('assets', '0009_auto_20191205_2041')]
dependencies = [
('assets', '0007_auto_20190108_0202_squashed_0014_auto_20191017_2052'),
]
operations = [
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.RunPython(
code=forwards,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
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.AlterField(
model_name='asset',
name='asset_id',
field=models.CharField(max_length=15, unique=True),
)
]

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

@@ -6,6 +6,11 @@ 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 Meta:
@@ -24,13 +29,15 @@ class AssetStatus(models.Model):
verbose_name_plural = 'Asset Statuses'
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.")
def __str__(self):
return self.name
class Supplier(models.Model):
@reversion.register
class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
class Meta:
@@ -55,7 +62,8 @@ class Connector(models.Model):
return self.description
class Asset(models.Model):
@reversion.register
class Asset(models.Model, RevisionMixin):
class Meta:
ordering = ['asset_id_prefix', 'asset_id_number']
permissions = (
@@ -63,7 +71,8 @@ class Asset(models.Model):
('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',
blank=True, null=True, on_delete=models.SET_NULL)
asset_id = models.CharField(max_length=15, unique=True)
description = models.CharField(max_length=120)
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
@@ -79,10 +88,14 @@ class Asset(models.Model):
# Cable assets
is_cable = models.BooleanField(default=False)
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')
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)
@@ -125,7 +138,8 @@ class Asset(models.Model):
self.asset_id = self.asset_id.upper()
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id)
if asset_search is None:
errdict["asset_id"] = ["An Asset ID can only consist of letters and numbers, with a final number"]
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:
errdict["purchase_price"] = ["A price cannot be negative"]

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

@@ -45,6 +45,16 @@
</div>
</form>
{% if not edit %}
<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 %}
{% 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

@@ -31,6 +31,7 @@
<td>{{ item.name }}</td>
<td>
<a href="{% url 'supplier_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
<a href="{% url 'supplier_history' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-time"></i> History</a>
</td>
</tr>
{% endfor %}

View File

@@ -1,5 +1,7 @@
from django.conf.urls import url
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
@@ -7,16 +9,27 @@ urlpatterns = [
path('', views.AssetList.as_view(), name='asset_index'),
path('asset/list/', views.AssetList.as_view(), name='asset_list'),
path('asset/id/<str:pk>/', views.AssetDetail.as_view(), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.add_asset')(views.AssetCreate.as_view()), name='asset_create'),
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/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'),
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/', 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('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
path('supplier/create', permission_required_with_403('assets.add_supplier')(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/create', permission_required_with_403('assets.add_supplier')
(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'),
]

View File

@@ -5,7 +5,9 @@ from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.urls import reverse
from django.db.models import Q
from django.shortcuts import get_object_or_404
from assets import models, forms
from RIGS import versioning
@method_decorator(csrf_exempt, name='dispatch')
@@ -36,7 +38,8 @@ class AssetList(LoginRequiredMixin, generic.ListView):
if len(query_string) == 0:
queryset = self.model.objects.all()
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))
else:
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:
queryset = queryset.filter(status__in=form.cleaned_data['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
@@ -203,3 +207,24 @@ class SupplierUpdate(generic.UpdateView):
model = models.Supplier
form_class = forms.SupplierForm
template_name = 'supplier_update.html'
class SupplierVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html"
# TODO: Reduce SQL queries
class AssetVersionHistory(versioning.VersionHistory):
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

@@ -5,7 +5,7 @@
{% endblock %}
{% block titleelements %}
{% if perms.assets.view_asset%}
{% if perms.assets.view_asset %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu">
@@ -16,7 +16,7 @@
</ul>
</li>
{% endif %}
{% if perms.assets.view_supplier%}
{% if perms.assets.view_supplier %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu">
@@ -28,4 +28,7 @@
</ul>
</li>
{% endif %}
{% if perms.assets.view_asset %}
<li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li>
{% endif %}
{% endblock %}