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;  | 
