diff options
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 |
commit | 40c94fba1a9e3e119af8738f75f1e8082feb0c8b (patch) | |
tree | 63a7d4c8f7cb34c6f4c0c9089bfafce00a46dfd8 | |
parent | e5568da593a5bc13ab2065ec22a2d6b035be756b (diff) | |
download | Ishtar-40c94fba1a9e3e119af8738f75f1e8082feb0c8b.tar.bz2 Ishtar-40c94fba1a9e3e119af8738f75f1e8082feb0c8b.zip |
Quick actions: generic urls, views and forms
-rw-r--r-- | archaeological_finds/forms.py | 112 | ||||
-rw-r--r-- | archaeological_finds/models_finds.py | 22 | ||||
-rw-r--r-- | archaeological_finds/urls.py | 15 | ||||
-rw-r--r-- | archaeological_finds/views.py | 52 | ||||
-rw-r--r-- | ishtar_common/forms.py | 15 | ||||
-rw-r--r-- | ishtar_common/models.py | 48 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar.js | 19 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/DataTables.html | 37 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/qa_form.html | 25 | ||||
-rw-r--r-- | ishtar_common/widgets.py | 5 | ||||
-rw-r--r-- | scss/custom.scss | 4 |
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; |