Skip to content
Snippets Groups Projects
Commit 7afe690c authored by Jean-Philippe Laverdure's avatar Jean-Philippe Laverdure
Browse files

Merge branch '56-keep-logs-of-activity-on-a-dataset' into 'master'

Resolve "Keep logs of activity on a dataset"

Closes #56

See merge request !24
parents 1d3298bc 26ad589a
No related branches found
No related tags found
1 merge request!24Resolve "Keep logs of activity on a dataset"
Showing with 735 additions and 2051 deletions
[flake8]
ignore = E501,W503
\ No newline at end of file
ignore = E501, E503
This diff is collapsed.
# Generated by Django 2.0.13 on 2019-10-11 19:00
import django.contrib.postgres.fields.hstore
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('portal', '0013_auto_20190807_1540'),
]
operations = [
migrations.CreateModel(
name='Log',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ts', models.DateTimeField(auto_now_add=True, verbose_name='Upload Timestamp')),
('type', models.CharField(choices=[('add', 'added'), ('remove', 'removed'), ('edit', 'edited')], max_length=64)),
('label_en', models.TextField(blank=True, null=True)),
('label_fr', models.TextField(blank=True, null=True)),
],
),
migrations.AlterField(
model_name='datafile',
name='annotations',
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, null=True, verbose_name='File Annotations'),
),
migrations.AddField(
model_name='log',
name='obj_datafile',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='portal.DataFile'),
),
migrations.AddField(
model_name='log',
name='obj_dataset',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='portal.DataSet'),
),
migrations.AddField(
model_name='log',
name='obj_group',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='portal.ShareGroup', verbose_name='Share with these groups'),
),
migrations.AddField(
model_name='log',
name='obj_lab',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='portal.Lab'),
),
migrations.AddField(
model_name='log',
name='obj_profile',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='portal.Profile'),
),
migrations.AddField(
model_name='log',
name='profile',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='portal.Profile'),
),
]
# Generated by Django 2.0.13 on 2019-10-16 14:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0014_auto_20191011_1500'),
]
operations = [
migrations.AlterField(
model_name='log',
name='type',
field=models.CharField(choices=[('add', 'added'), ('remove', 'removed'), ('add_share', 'shared with'), ('remove_share', 'removed sharing with'), ('edit', 'edited')], max_length=64),
),
]
# Generated by Django 2.0.13 on 2019-10-16 14:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('portal', '0015_auto_20191016_1041'),
]
operations = [
migrations.RenameField(
model_name='log',
old_name='type',
new_name='action_type',
),
]
......@@ -7,6 +7,7 @@ from django.contrib.auth.models import User
from django.contrib.postgres.fields import HStoreField
from django.db import models
from django.db.models import Q
from django.template import defaultfilters
from django.utils.translation import get_language
from django.utils.translation import ugettext_lazy as _
from private_storage.fields import PrivateFileField
......@@ -25,6 +26,7 @@ class Profile(models.Model):
"""
class Meta:
ordering = ['user__last_name', 'user__first_name']
verbose_name = _("user")
accountname = models.CharField(max_length=150, null=True, blank=True)
unix_username = models.CharField(max_length=150, null=True, blank=True, verbose_name=_("Unix username"))
......@@ -93,7 +95,6 @@ class Lab(models.Model):
return self.name
class DataFileManager(models.Manager):
def limit_to_profile(self, profile):
return super().get_queryset().filter(uploaded_by=profile)
......@@ -134,6 +135,10 @@ class DataFile(models.Model):
Also of interest:
https://www.citusdata.com/blog/2016/07/14/choosing-nosql-hstore-json-jsonb/
"""
class Meta:
verbose_name = _("file")
# Allow passing an NFS path at init?
# def __init__(self, src):
# self.upload_from_nfs(src)
......@@ -264,6 +269,51 @@ class DataSet(models.Model):
return self.name
# class LogActionType(models.Model):
# name = models.CharField(max_length=64, null=False, blank=False, unique=True, primary_key=True, verbose_name=_('Action'))
class Log(models.Model):
TYPE_CHOICES = (
('add', _('added')),
('remove', _('removed')),
('add_share', _('shared with')),
('remove_share', _('removed sharing with')),
('edit', _('edited')),
)
ts = models.DateTimeField(verbose_name=_('Upload Timestamp'), auto_now_add=True)
action_type = models.CharField(max_length=64, choices=TYPE_CHOICES, null=False)
label_en = models.TextField(null=True, blank=True)
label_fr = models.TextField(null=True, blank=True)
profile = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='logs')
obj_dataset = models.ForeignKey(DataSet, null=True, on_delete=models.PROTECT, related_name='logs')
obj_profile = models.ForeignKey(Profile, null=True, on_delete=models.PROTECT)
obj_lab = models.ForeignKey(Lab, null=True, on_delete=models.PROTECT)
obj_group = models.ForeignKey(ShareGroup, null=True, on_delete=models.PROTECT, verbose_name=_('Share with these groups'))
obj_datafile = models.ForeignKey(DataFile, null=True, on_delete=models.PROTECT)
@property
def m2m_obj(self):
obj = {self.obj_profile, self.obj_lab, self.obj_group, self.obj_datafile}
obj.remove(None)
if len(obj) == 1:
return obj.pop()
return None
def __str__(self):
if self.action_type:
action = self.get_action_type_display()
if self.obj_dataset:
if self.action_type == 'edit':
return _('{} ago : {} {} "{}"').format(defaultfilters.timesince_filter(self.ts), self.profile.fullname, action, self.label_en if get_language() == 'en' else self.label_fr)
if self.m2m_obj:
return _('{} ago : {} {} {} "{}"').format(defaultfilters.timesince_filter(self.ts), self.profile.fullname, action, self.m2m_obj.__class__._meta.verbose_name, self.m2m_obj)
return super().__str__()
class DataTypes(models.Model):
"""
DataTypes are a specialization of FSFile for which a set of annotation keys are pre-defined
......
......@@ -10,6 +10,9 @@
<li class="nav-item">
<a class="nav-link" href="#settings" id="settings-tab" data-toggle="tab" role="tab" aria-controls="settings" aria-selected="false">{% trans 'Sharing Parameters' %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#log" id="log-tab" data-toggle="tab" role="tab" aria-controls="log" aria-selected="false">{% trans 'Log' %}</a>
</li>
</ul>
</div>
<div class="card-body tab-content">
......@@ -19,48 +22,11 @@
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="settings-tab">
<dl class="row">
<dt class="col-sm-3">{% trans 'Shared by' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
<li>{{ object.created_by.fullname }}</li>
</ul>
</dd>
<dt class="col-sm-3">{% trans 'Shared with profiles' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for profile in object.share_profiles.all %}
<li>{{ profile.fullname }}</li>
{% empty %}
<li class="text-warning">{% trans 'No specified profiles'%}</li>
{% endfor %}
</ul>
</dd>
<dt class="col-sm-3">{% trans 'Shared with labs' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for lab in object.share_labs.all %}
<li>{{ lab.name }}</li>
{% empty %}
<li class="text-warning">{% trans 'No specified labs'%}</li>
{% endfor %}
</ul>
</dd>
{% include 'portal/widgets/dataset_sharing.html' %}
</div>
<dt class="col-sm-3">{% trans 'Shared with groups' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for group in object.share_groups.all %}
<li>{{ group.name }}</li>
{% empty %}
<li class="text-warning">{% trans 'No specified share groups'%}</li>
{% endfor %}
</ul>
</dd>
</dl>
<div class="tab-pane" id="log" role="tabpanel" aria-labelledby="log-tab">
{% include 'portal/widgets/dataset_log.html' %}
</div>
</div>
......
{% load i18n %}
{% for log in object.logs.all %}
<div class="row">
<div class="col">{{log}}</div>
</div>
{% endfor %}
{% load i18n %}
<dl class="row">
<dt class="col-sm-3">{% trans 'Shared by' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
<li>{{ object.created_by.fullname }}</li>
</ul>
</dd>
<dt class="col-sm-3">{% trans 'Shared with profiles' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for profile in object.share_profiles.all %}
<li>{{ profile.fullname }}</li>
{% empty %}
<li class="text-warning">{% trans 'No specified profiles'%}</li>
{% endfor %}
</ul>
</dd>
<dt class="col-sm-3">{% trans 'Shared with labs' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for lab in object.share_labs.all %}
<li>{{ lab.name }}</li>
{% empty %}
<li class="text-warning">{% trans 'No specified labs'%}</li>
{% endfor %}
</ul>
</dd>
<dt class="col-sm-3">{% trans 'Shared with groups' %}:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for group in object.share_groups.all %}
<li>{{ group.name }}</li>
{% empty %}
<li class="text-warning">{% trans 'No specified share groups'%}</li>
{% endfor %}
</ul>
</dd>
</dl>
import copy
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import JsonResponse
from django.utils import translation
from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView
from django.views.generic.base import ContextMixin
from django.views.generic.detail import BaseDetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.utils.translation import ugettext_lazy as _
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import UserPassesTestMixin
from django.views.generic.edit import ModelFormMixin
import copy
from ..models import AppSettings
from ..models import (AppSettings, DataFile, DataSet, Lab, Log, Profile,
ShareGroup)
class StaffViewMixin(UserPassesTestMixin):
......@@ -119,3 +121,51 @@ class JSONView(BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return JsonResponse({'data': self.get_data()}, **response_kwargs)
class LogMixin(ModelFormMixin):
def form_valid(self, form):
before = self.get_object()
for d in self.qs_params:
old = getattr(before, d)
new = form.cleaned_data[d]
if old != new:
removed = set(old.all()) - set(new.all())
added = set(new.all()) - set(old.all())
for o in added.union(removed):
type = 'remove'
if o in added:
type = 'add'
with translation.override('en'):
label_en = str(form.fields[d].label)
with translation.override('fr'):
label_fr = str(form.fields[d].label)
log = Log(action_type=type, profile=self.request.user.profile, obj_dataset=before, label_fr=label_fr, label_en=label_en)
if isinstance(o, Profile):
log.action_type += '_share'
log.obj_profile = o
elif isinstance(o, Lab):
log.action_type += '_share'
log.obj_lab = o
elif isinstance(o, ShareGroup):
log.action_type += '_share'
log.obj_group = o
elif isinstance(o, DataFile):
log.obj_datafile = o
log.save()
for d in form.changed_data:
if d not in self.qs_params:
with translation.override('en'):
label_en = str(form.fields['name'].label)
with translation.override('fr'):
label_fr = str(form.fields['name'].label)
Log.objects.create(action_type='edit', profile=self.request.user.profile, obj_dataset=before, label_fr=label_fr, label_en=label_en)
return super().form_valid(form)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateView
from ...models import Alert
from ...views import ActivePageViewMixin, LoginRequiredMixin, StaffViewMixin
from portal.models import Alert
from portal.views import ActivePageViewMixin, StaffViewMixin
class DashboardView(LoginRequiredMixin, ActivePageViewMixin, TemplateView):
......
......@@ -12,11 +12,11 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DeleteView, DetailView,
TemplateView, UpdateView)
from ...forms import DataSetForm, DataSetDisplayFieldsForm
from ...models import DataFile, DataSet, Profile, ShareGroup, Lab
from ...views import (ActivePageViewMixin, AjaxDatatableBackboneMixin,
CreateViewMixin, DeleteViewMixin, JSONListView,
StaffViewMixin, UpdateViewMixin)
from portal.forms import DataSetForm, DataSetDisplayFieldsForm
from portal.models import DataFile, DataSet, Lab, Profile, ShareGroup
from portal.views import (ActivePageViewMixin, AjaxDatatableBackboneMixin,
CreateViewMixin, DeleteViewMixin, JSONListView,
LogMixin, StaffViewMixin, UpdateViewMixin)
logger = logging.getLogger('debug')
......@@ -69,6 +69,10 @@ class DataSetJSONListView(LoginRequiredMixin, JSONListView):
return rows
class DataSetLogMixin(LogMixin):
qs_params = ['share_profiles', 'share_labs', 'files', 'share_groups']
class DataSetCreateView(LoginRequiredMixin, CreateViewMixin, CreateView):
model = DataSet
form_class = DataSetForm
......@@ -91,7 +95,7 @@ class DataSetCreateView(LoginRequiredMixin, CreateViewMixin, CreateView):
return super().form_valid(form)
class DataSetUpdateView(LoginRequiredMixin, UpdateViewMixin, UpdateView):
class DataSetUpdateView(DataSetLogMixin, LoginRequiredMixin, UpdateViewMixin, UpdateView):
model = DataSet
form_class = DataSetForm
success_url = reverse_lazy('user.datasets')
......
......@@ -11,12 +11,11 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DeleteView, TemplateView,
UpdateView)
from ...forms import ShareGroupForm
from ...models import DataFile, ShareGroup, Profile
from ...views import (ActivePageViewMixin, AjaxDatatableBackboneMixin,
CreateViewMixin, DeleteViewMixin, JSONListView,
StaffViewMixin, UpdateViewMixin)
from portal.forms import ShareGroupForm
from portal.models import Profile, ShareGroup
from portal.views import (ActivePageViewMixin, AjaxDatatableBackboneMixin,
CreateViewMixin, DeleteViewMixin, JSONListView,
StaffViewMixin, UpdateViewMixin)
logger = logging.getLogger('debug')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment