summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2018-09-05 10:41:24 +0200
committerÉtienne Loks <etienne.loks@iggdrasil.net>2018-10-24 12:06:08 +0200
commit40c94fba1a9e3e119af8738f75f1e8082feb0c8b (patch)
tree63a7d4c8f7cb34c6f4c0c9089bfafce00a46dfd8
parente5568da593a5bc13ab2065ec22a2d6b035be756b (diff)
downloadIshtar-40c94fba1a9e3e119af8738f75f1e8082feb0c8b.tar.bz2
Ishtar-40c94fba1a9e3e119af8738f75f1e8082feb0c8b.zip
Quick actions: generic urls, views and forms
-rw-r--r--archaeological_finds/forms.py112
-rw-r--r--archaeological_finds/models_finds.py22
-rw-r--r--archaeological_finds/urls.py15
-rw-r--r--archaeological_finds/views.py52
-rw-r--r--ishtar_common/forms.py15
-rw-r--r--ishtar_common/models.py48
-rw-r--r--ishtar_common/static/js/ishtar.js19
-rw-r--r--ishtar_common/templates/blocks/DataTables.html37
-rw-r--r--ishtar_common/templates/ishtar/qa_form.html25
-rw-r--r--ishtar_common/widgets.py5
-rw-r--r--scss/custom.scss4
11 files changed, 314 insertions, 40 deletions
diff --git a/archaeological_finds/forms.py b/archaeological_finds/forms.py
index fbcc32013..a7fc0bc02 100644
--- a/archaeological_finds/forms.py
+++ b/archaeological_finds/forms.py
@@ -52,7 +52,7 @@ from bootstrap_datepicker.widgets import DatePicker
from ishtar_common import widgets
from ishtar_common.forms import CustomForm, CustomFormSearch, FormSet, \
FloatField, reverse_lazy, TableSelect, get_now, FinalForm, \
- ManageOldType, FieldType, IshtarForm, FormHeader
+ ManageOldType, FieldType, IshtarForm, FormHeader, QAForm
from ishtar_common.forms_common import get_town_field
from ishtar_common.models import valid_id, valid_ids, get_current_profile, \
SpatialReferenceSystem, Area, OperationType
@@ -79,6 +79,7 @@ __all__ = [
'check_treatment', 'ResultFindForm', 'ResultFindFormSet',
'FindDeletionForm', 'UpstreamFindFormSelection', 'NewFindBasketForm',
'SelectFindBasketForm', 'DeleteFindBasketForm', 'FindBasketAddItemForm',
+ 'QAFindFormSingle', 'QAFindFormMulti'
]
logger = logging.getLogger(__name__)
@@ -207,24 +208,24 @@ class FindForm(CustomForm, ManageOldType):
dimensions_comment = forms.CharField(
label=_(u"Dimensions comment"), required=False, widget=forms.Textarea)
- HEADERS['get_first_base_find__topographic_localisation'] = FormHeader(
+ HEADERS['get_first_base_find__x'] = FormHeader(
_(u"Coordinates"))
- get_first_base_find__topographic_localisation = forms.CharField(
- label=_(u"Point of topographic reference"),
- required=False, max_length=120
- )
get_first_base_find__x = forms.FloatField(label=_(u"X"), required=False)
- get_first_base_find__y = forms.FloatField(label=_(u"Y"), required=False)
- get_first_base_find__z = forms.FloatField(label=_(u"Z"), required=False)
- get_first_base_find__spatial_reference_system = \
- forms.ChoiceField(label=_(u"Spatial Reference System"), required=False,
- choices=[])
get_first_base_find__estimated_error_x = \
forms.FloatField(label=_(u"Estimated error for X"), required=False)
+ get_first_base_find__y = forms.FloatField(label=_(u"Y"), required=False)
get_first_base_find__estimated_error_y = \
forms.FloatField(label=_(u"Estimated error for Y"), required=False)
+ get_first_base_find__z = forms.FloatField(label=_(u"Z"), required=False)
get_first_base_find__estimated_error_z = \
forms.FloatField(label=_(u"Estimated error for Z"), required=False)
+ get_first_base_find__spatial_reference_system = \
+ forms.ChoiceField(label=_(u"Spatial Reference System"), required=False,
+ choices=[])
+ get_first_base_find__topographic_localisation = forms.CharField(
+ label=_(u"Point of topographic reference"),
+ required=False, max_length=120
+ )
HEADERS['checked_type'] = FormHeader(_(u"Sheet"))
checked_type = forms.ChoiceField(label=_(u"Check"), required=False)
@@ -315,6 +316,95 @@ class FindForm(CustomForm, ManageOldType):
return self.cleaned_data
+QAHeaders = {
+ 'description': FormHeader(_(u"Description")),
+ 'checked_type': FormHeader(_(u"Sheet"))
+}
+
+
+class QAFindFormMulti(QAForm):
+ form_admin_name = _(u"Find - Quick action - Modify")
+ form_slug = "find-quickaction-modify"
+ base_models = ['get_first_base_find', 'object_types', 'material_types',
+ 'communicabilities']
+ associated_models = {
+ 'material_types': models.MaterialType,
+ 'object_types': models.ObjectType,
+ 'communicabilities': models.CommunicabilityType,
+ 'checked_type': models.CheckedType,
+ }
+
+ MULTI = True
+ REPLACE_FIELDS = [
+ 'manufacturing_place', 'checked_type', 'check_date'
+ ]
+
+ HEADERS = QAHeaders.copy()
+
+ description = forms.CharField(label=_(u"Description"),
+ widget=forms.Textarea, required=False)
+ material_types = widgets.Select2MultipleField(
+ label=_(u"Material types"), required=False
+ )
+ object_types = widgets.Select2MultipleField(
+ label=_(u"Object types"), required=False,
+ )
+ decoration = forms.CharField(
+ label=_(u"Decoration"), widget=forms.Textarea,
+ required=False)
+ inscription = forms.CharField(
+ label=_(u"Inscription"), widget=forms.Textarea,
+ required=False)
+ manufacturing_place = forms.CharField(
+ label=_(u"Manufacturing place"), required=False)
+ communicabilities = widgets.Select2MultipleField(
+ label=_(u"Communicability"), required=False
+ )
+ comment = forms.CharField(
+ label=_(u"Comment"), required=False,
+ widget=forms.Textarea)
+ dating_comment = forms.CharField(
+ label=_(u"Comment on dating"), required=False,
+ widget=forms.Textarea)
+
+ checked_type = forms.ChoiceField(label=_(u"Check"), required=False)
+ check_date = forms.DateField(
+ initial=get_now, label=_(u"Check date"), widget=DatePicker)
+
+ TYPES = [
+ FieldType('material_types', models.MaterialType, is_multiple=True),
+ FieldType('object_types', models.ObjectType, is_multiple=True),
+ FieldType('communicabilities', models.CommunicabilityType,
+ is_multiple=True),
+ FieldType('checked_type', models.CheckedType, is_multiple=True),
+ ]
+
+
+class QAFindFormSingle(QAFindFormMulti):
+ form_admin_name = _(u"Find - Quick action - Modify single")
+ form_slug = "find-quickaction-modifysingle"
+ HEADERS = QAHeaders.copy()
+ HEADERS['label'] = FormHeader(_(u"Identification"))
+
+ label = forms.CharField(
+ label=_(u"Free ID"),
+ validators=[validators.MaxLengthValidator(60)])
+ denomination = forms.CharField(label=_(u"Denomination"), required=False)
+ previous_id = forms.CharField(label=_("Previous ID"), required=False)
+ get_first_base_find__excavation_id = forms.CharField(
+ label=_(u"Excavation ID"), required=False)
+ museum_id = forms.CharField(label=_(u"Museum ID"), required=False)
+ seal_number = forms.CharField(label=_(u"Seal number"), required=False)
+ mark = forms.CharField(label=_(u"Mark"), required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(QAFindFormSingle, self).__init__(*args, **kwargs)
+ if not self.items or \
+ not self.items[0].get_first_base_find(
+ ).context_record.operation.operation_type.judiciary:
+ self.fields.pop('seal_number')
+
+
class PreservationForm(CustomForm, ManageOldType):
form_label = _("Preservation")
form_admin_name = _(u"Find - 030 - Preservation")
diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py
index 1ef9d5846..0911317f2 100644
--- a/archaeological_finds/models_finds.py
+++ b/archaeological_finds/models_finds.py
@@ -39,7 +39,7 @@ from ishtar_common.models import Document, GeneralType, \
HierarchicalType, BaseHistorizedItem, ShortMenuItem, LightHistorizedItem, \
HistoricalRecords, OwnPerms, Person, Basket, post_save_cache, \
ValueGetter, get_current_profile, IshtarSiteProfile, PRIVATE_FIELDS, \
- SpatialReferenceSystem, BulkUpdatedItem, ExternalIdManager
+ SpatialReferenceSystem, BulkUpdatedItem, ExternalIdManager, QuickAction
from archaeological_operations.models import AdministrativeAct, Operation
from archaeological_context_records.models import ContextRecord, Dating
@@ -605,7 +605,7 @@ class FBulkView(object):
class Find(BulkUpdatedItem, ValueGetter, BaseHistorizedItem, OwnPerms,
- ShortMenuItem):
+ MainItem):
EXTERNAL_ID_KEY = 'find_external_id'
SHOW_URL = 'show-find'
SLUG = 'find'
@@ -878,6 +878,24 @@ class Find(BulkUpdatedItem, ValueGetter, BaseHistorizedItem, OwnPerms,
"remarkabilities__label", "material_types__label"]
objects = ExternalIdManager()
+ QA_EDIT = QuickAction(
+ url="find-qa-bulk-update", icon_class="fa fa-pencil",
+ text=_(u"Bulk update"), target="many",
+ rights=['change_find', 'change_own_find'])
+
+ QUICK_ACTIONS = [
+ QA_EDIT,
+ QuickAction(
+ url="find-qa-packaging", icon_class="fa fa-gift",
+ text=_(u"Packaging"), target="many", rights=['change_warehouse'],
+ module='warehouse'
+ ),
+ QuickAction(
+ url="find-qa-basket", icon_class="fa fa-shopping-basket",
+ text=_(u"Basket"), target="many",
+ rights=['change_find', 'change_own_find']),
+ ]
+
# fields
base_finds = models.ManyToManyField(BaseFind, verbose_name=_(u"Base find"),
related_name='find')
diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py
index 9a71c66d8..39da45532 100644
--- a/archaeological_finds/urls.py
+++ b/archaeological_finds/urls.py
@@ -71,6 +71,21 @@ urlpatterns = [
check_rights(['change_find', 'change_own_find'])(
views.DeleteFindBasketView.as_view()), name='delete_findbasket'),
+ url(r'^find-qa-bulk-update/(?P<pks>[0-9-]+)?/$',
+ check_rights(['change_find', 'change_own_find'])(
+ views.QAFindForm.as_view()),
+ name='find-qa-bulk-update'),
+
+ url(r'^find-qa-packaging/(?P<pks>[0-9-]+)?/$',
+ check_rights(['change_warehouse'])(
+ views.FindBasketAddItemView.as_view()),
+ name='find-qa-packaging'),
+ url(r'^find-qa-basket/(?P<pks>[0-9-]+)?/$',
+ check_rights(['change_find', 'change_own_find'])(
+ views.FindBasketAddItemView.as_view()),
+ name='find-qa-basket'),
+
+
url(r'^treatment_creation/(?P<step>.+)?$',
check_rights(['change_find', 'change_own_find'])(
views.treatment_creation_wizard), name='treatment_creation'),
diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py
index 6c6d9fff9..034e1994f 100644
--- a/archaeological_finds/views.py
+++ b/archaeological_finds/views.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-# Copyright (C) 2010-2016 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+# Copyright (C) 2010-2018 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -21,7 +21,7 @@ import json
from django.core.urlresolvers import reverse
from django.db.models import Q
-from django.http import HttpResponseRedirect, HttpResponse
+from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
@@ -594,3 +594,51 @@ def reset_wizards(request):
'treatmentfle_admacttreatmentfle_deletion'),
):
wizard_class.session_reset(request, url_name)
+
+
+class QAItemEditForm(IshtarMixin, LoginRequiredMixin, FormView):
+ template_name = 'ishtar/qa_form.html'
+ model = None
+ form_class = None
+ form_class_multi = None
+
+ def dispatch(self, request, *args, **kwargs):
+ assert self.model
+ pks = [int(pk) for pk in kwargs.get('pks').split('-')]
+ self.items = list(self.model.objects.filter(pk__in=pks))
+ if not self.items:
+ raise Http404()
+
+ # check availability
+ if not self.model.QA_EDIT.is_available(
+ user=request.user, session=request.session):
+ for item in self.items:
+ if not self.model.QA_EDIT.is_available(
+ user=request.user, session=request.session, obj=item):
+ raise Http404()
+
+ return super(QAItemEditForm, self).dispatch(request, *args, **kwargs)
+
+ def get_form_class(self):
+ if len(self.items) > 1:
+ return self.form_class_multi
+ return self.form_class
+
+ def get_form_kwargs(self):
+ kwargs = super(QAItemEditForm, self).get_form_kwargs()
+ kwargs['items'] = self.items
+ return kwargs
+
+
+class QAFindForm(QAItemEditForm):
+ model = models.Find
+ form_class = QAFindFormSingle
+ form_class_multi = QAFindFormMulti
+
+ def get_success_url(self, basket):
+ return reverse('select_itemsinbasket',
+ kwargs={'pk': basket})
+
+ def form_valid(self, form):
+ return HttpResponseRedirect(self.get_success_url(
+ form.cleaned_data['basket']))
diff --git a/ishtar_common/forms.py b/ishtar_common/forms.py
index 3dfcad09e..4300e9c36 100644
--- a/ishtar_common/forms.py
+++ b/ishtar_common/forms.py
@@ -640,6 +640,21 @@ class ManageOldType(IshtarForm):
self.fields[field.key].help_text = field.get_help()
+class QAForm(CustomForm, ManageOldType):
+ MULTI = False
+
+ def __init__(self, *args, **kwargs):
+ self.items = kwargs.pop('items')
+ super(QAForm, self).__init__(*args, **kwargs)
+ for k in self.fields:
+ if self.MULTI and k not in self.REPLACE_FIELDS:
+ self.fields[k].label = unicode(self.fields[k].label) + \
+ unicode(u" - append to existing")
+ else:
+ self.fields[k].label = unicode(self.fields[k].label) + \
+ unicode(u" - replace")
+
+
class DocumentGenerationForm(forms.Form):
"""
Form to generate document by choosing the template
diff --git a/ishtar_common/models.py b/ishtar_common/models.py
index 5141ed66d..45ce9f504 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -1661,52 +1661,68 @@ class QuickAction(object):
"""
Quick action available from tables
"""
- def __init__(self, url, icon='', text='', target=None, rights=None):
+ def __init__(self, url, icon_class='', text='', target=None, rights=None,
+ module=None):
self.url = url
- self.icon = icon
+ self.icon_class = icon_class
self.text = text
self.rights = rights
self.target = target
+ self.module = module
assert self.target in ('one', 'many', None)
def is_available(self, user, session=None, obj=None):
+ if self.module and not getattr(get_current_profile(), self.module):
+ return False
if not self.rights: # no restriction
return True
+ if not user or not hasattr(user, 'ishtaruser') or not user.ishtaruser:
+ return False
+ user = user.ishtaruser
+
for right in self.rights:
if user.has_perm(right, session=session, obj=obj):
return True
return False
- def render(self):
- lbl = self.text
- if self.icon:
- lbl = self.icon
+ @property
+ def rendered_icon(self):
+ if not self.icon_class:
+ return ""
+ return u"<i class='{}' aria-hidden='true'></i>".format(self.icon_class)
+
+ @property
+ def base_url(self):
if self.target is None:
url = reverse(self.url)
else:
# put arbitrary pk for the target
url = reverse(self.url, args=[0])
- url = url[:-1] # all quick action url have to finish with the
- # pk of the selected item
- return u'<a href="#" data-url="{}" title="{}">{}</a>'.format(
- url, self.text, lbl
- )
+ url = url[:-2] # all quick action url have to finish with the
+ # pk of the selected item and a "/"
+ return url
class MainItem(ShortMenuItem):
"""
- Item with quick actions availables from tables
+ Item with quick actions available from tables
"""
QUICK_ACTIONS = []
@classmethod
- def render_quick_actions(cls, user, session=None, obj=None):
- rendered = []
+ def get_quick_actions(cls, user, session=None, obj=None):
+ """
+ Get a list of (url, title, icon, target) actions for an user
+ """
+ qas = []
for action in cls.QUICK_ACTIONS:
if not action.is_available(user, session=session, obj=obj):
continue
- rendered.append(action.render())
- return mark_safe(u" ".join(rendered))
+ qas.append([action.base_url,
+ mark_safe(action.text),
+ mark_safe(action.rendered_icon),
+ action.target or ""])
+ return qas
class LightHistorizedItem(BaseHistorizedItem):
diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js
index f05eb5a1b..119229e8d 100644
--- a/ishtar_common/static/js/ishtar.js
+++ b/ishtar_common/static/js/ishtar.js
@@ -761,3 +761,22 @@ function manage_pinned_search(name, data){
}
}
+var dt_generate_qa_url = function (table, url){
+ var data = table.rows( { selected: true } ).data();
+ var value = "";
+ for (k in data){
+ if (!data[k]['id']) continue;
+ if (k > 0) value += "-";
+ value += data[k]['id'];
+ }
+ url += value + "/";
+ return url;
+}
+
+var dt_qa_open = function (url){
+ long_wait();
+ $('#modal-dynamic-form').load(url, function(){
+ $('#modal-dynamic-form').modal("show");
+ close_wait();
+ });
+}
diff --git a/ishtar_common/templates/blocks/DataTables.html b/ishtar_common/templates/blocks/DataTables.html
index 70d47cb8d..7fad1f121 100644
--- a/ishtar_common/templates/blocks/DataTables.html
+++ b/ishtar_common/templates/blocks/DataTables.html
@@ -162,16 +162,35 @@ jQuery(document).ready(function(){
"select": {
"style": {% if multiple_select %}'multi'{% else %}'single'{% endif %}
},
- {% if multiple_select %}"buttons": [
- 'selectAll',
- 'selectNone'
- ],
- "language": {
- buttons: {
- selectAll: "{% trans 'Select all items' %}",
- selectNone: "{% trans 'Select none' %}"
+ {% if multiple_select or quick_actions %}"buttons": [
+ {% for url, title, icon, target in quick_actions %}
+ {
+ {% if target == 'one' %}extend: 'selectedSingle',
+ {% elif target == 'many' %}extend: 'selected',
+ {% endif %}
+ className: "btn btn-success",
+ text: "{{icon}}",
+ titleAttr: "{{title}}",
+ action: function (e, dt, node, config) {
+ var url = dt_generate_qa_url(dt, "{{url}}");
+ dt_qa_open(url);
+ return false;
+ }
+ },
+ {% if not forloop.last %},{% endif %}
+ {% endfor %}{% if multiple_select %}{% if quick_actions%},{% endif %}
+ {
+ extend: 'selectAll',
+ text: '<i class="fa fa-check-circle-o"></i>',
+ titleAttr: "{% trans 'Select all items' %}"
+ },
+ {
+ extend: 'selectNone',
+ text: '<i class="fa fa-times"></i>',
+ titleAttr: "{% trans 'Deselect' %}"
}
- },
+ {% endif %}
+ ],
"dom": 'lBtip',
{% else %}
"dom": 'ltip',
diff --git a/ishtar_common/templates/ishtar/qa_form.html b/ishtar_common/templates/ishtar/qa_form.html
new file mode 100644
index 000000000..95f8887a8
--- /dev/null
+++ b/ishtar_common/templates/ishtar/qa_form.html
@@ -0,0 +1,25 @@
+{% load i18n inline_formset table_form %}
+
+<div class="modal-dialog modal-lg modal-dialog-centered">
+ <div class="modal-content" id='progress-content'>
+ <div class="modal-header">
+ <h2>{{page_name}}</h2>
+ </div>
+ <form enctype="multipart/form-data" action="." method="post">{% csrf_token %}
+ <div class="modal-body">
+ <div class='form'>
+ {% for error in form.non_field_errors %}
+ <p>{{ error }}</p>
+ {% endfor %}
+ {% bs_form form %}
+ </div>
+ <button type="submit" id="submit_form" name='validate'
+ value="validate" class="btn btn-success">
+ {% trans "Modify" %}
+ </button>
+ </div>
+ </form>
+ </div>
+</div>
+
+
diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py
index fc8926364..b5e1f5891 100644
--- a/ishtar_common/widgets.py
+++ b/ishtar_common/widgets.py
@@ -1033,6 +1033,7 @@ class DataTable(Select2Media, forms.RadioSelect):
# dct['source_full'] = unicode(self.source_full)
dct['extra_sources'] = []
+ dct['quick_actions'] = []
if self.associated_model:
model_name = "{}.{}".format(
self.associated_model.__module__,
@@ -1043,6 +1044,10 @@ class DataTable(Select2Media, forms.RadioSelect):
dct['extra_sources'].append((
imp.slug, imp.name,
reverse('get-by-importer', args=[imp.slug])))
+ if hasattr(self.associated_model, "QUICK_ACTIONS"):
+ dct['quick_actions'] = \
+ self.associated_model.get_quick_actions(user=self.user)
+ self.multiple_select = True
source = unicode(self.source)
dct.update({'name': name,
'col_names': col_names,
diff --git a/scss/custom.scss b/scss/custom.scss
index 2406a1d98..e265b61e7 100644
--- a/scss/custom.scss
+++ b/scss/custom.scss
@@ -168,6 +168,10 @@ div.dt-buttons{
@include transition($btn-transition);
}
+.dt-button.btn-success {
+ @include button-variant($success, $success);
+}
+
.dt-button.disabled{
background-color: #f8f9fa;
border-color: #f8f9fa;