diff options
Diffstat (limited to 'ishtar_common')
| -rw-r--r-- | ishtar_common/admin.py | 138 | ||||
| -rw-r--r-- | ishtar_common/forms.py | 126 | ||||
| -rw-r--r-- | ishtar_common/forms_common.py | 19 | ||||
| -rw-r--r-- | ishtar_common/migrations/0021_auto_20171110_1717.py | 77 | ||||
| -rw-r--r-- | ishtar_common/migrations/0022_customform.py | 32 | ||||
| -rw-r--r-- | ishtar_common/migrations/0023_excludedfield.py | 28 | ||||
| -rw-r--r-- | ishtar_common/migrations/0024_custom_form_enabled.py | 24 | ||||
| -rw-r--r-- | ishtar_common/models.py | 129 | ||||
| -rw-r--r-- | ishtar_common/tests.py | 72 | ||||
| -rw-r--r-- | ishtar_common/utils.py | 25 | ||||
| -rw-r--r-- | ishtar_common/wizards.py | 55 | 
11 files changed, 655 insertions, 70 deletions
| diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index d3292148f..fa71c4d3f 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -31,6 +31,8 @@ from django.contrib.contenttypes.models import ContentType  from django.contrib.sites.admin import SiteAdmin  from django.contrib.sites.models import Site  from django.contrib.gis.forms import PointField, OSMWidget, MultiPolygonField +from django.core.cache import cache +from django.forms import BaseInlineFormSet  from django.http import HttpResponseRedirect, HttpResponse  from django.shortcuts import render  from django.template.defaultfilters import slugify @@ -38,8 +40,23 @@ from django.utils.translation import ugettext_lazy as _  from django import forms -from ishtar_common.apps import admin_site  from ishtar_common import models +from ishtar_common.apps import admin_site +from ishtar_common.utils import get_cache + +from ishtar_common import forms as common_forms +from archaeological_files import forms as file_forms +from archaeological_files_pdl import forms as file_pdl_forms +from archaeological_operations import forms as operation_forms +from archaeological_context_records import forms as context_record_forms +from archaeological_finds import forms as find_forms, \ +    forms_treatments as treatment_forms +from archaeological_warehouse import forms as warehouse_forms + + +ISHTAR_FORMS = [common_forms, file_pdl_forms, file_forms, operation_forms, +                context_record_forms, find_forms, treatment_forms, +                warehouse_forms]  class ImportGenericForm(forms.Form): @@ -76,6 +93,7 @@ def gen_import_generic(self, request, queryset):                       request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})      return render(request, 'admin/import_from_csv.html', {'csv_form': form}) +  gen_import_generic.short_description = "Import from a CSV file" @@ -151,6 +169,7 @@ class IshtarSiteProfileAdmin(admin.ModelAdmin):                      'find', 'warehouse', 'mapping', 'preservation')      model = models.IshtarSiteProfile +  admin_site.register(models.IshtarSiteProfile, IshtarSiteProfileAdmin) @@ -158,6 +177,7 @@ class DepartmentAdmin(admin.ModelAdmin):      list_display = ('number', 'label',)      model = models.Department +  admin_site.register(models.Department, DepartmentAdmin) @@ -168,6 +188,7 @@ class OrganizationAdmin(HistorizedObjectAdmin):      exclude = ('merge_key', 'merge_exclusion', 'merge_candidate', )      model = models.Organization +  admin_site.register(models.Organization, OrganizationAdmin) @@ -179,6 +200,7 @@ class PersonAdmin(HistorizedObjectAdmin):      form = make_ajax_form(models.Person, {'attached_to': 'organization'})      model = models.Person +  admin_site.register(models.Person, PersonAdmin) @@ -236,6 +258,7 @@ class AuthorAdmin(admin.ModelAdmin):      model = models.Author      form = make_ajax_form(models.Author, {'person': 'person'}) +  admin_site.register(models.Author, AuthorAdmin) @@ -244,11 +267,14 @@ class PersonTypeAdmin(admin.ModelAdmin):      model = models.PersonType      filter_vertical = ('groups',) +  admin_site.register(models.PersonType, PersonTypeAdmin)  class GlobalVarAdmin(admin.ModelAdmin):      list_display = ['slug', 'description', 'value'] + +  admin_site.register(models.GlobalVar, GlobalVarAdmin) @@ -275,16 +301,22 @@ class ImporterDefaultAdmin(admin.ModelAdmin):      list_display = ('importer_type', 'target')      model = models.ImporterDefault      inlines = (ImporterDefaultValuesInline,) + +  admin_site.register(models.ImporterDefault, ImporterDefaultAdmin)  class ImporterTypeAdmin(admin.ModelAdmin):      list_display = ('name', 'associated_models', 'available') + +  admin_site.register(models.ImporterType, ImporterTypeAdmin)  class RegexpAdmin(admin.ModelAdmin):      list_display = ('name', 'description', "regexp") + +  admin_site.register(models.Regexp, RegexpAdmin) @@ -312,6 +344,8 @@ class ImporterColumnAdmin(admin.ModelAdmin):                      'targets_lbl', 'duplicate_fields_lbl', 'required')      list_filter = ('importer_type',)      inlines = (ImportTargetInline, ImporterDuplicateFieldInline) + +  admin_site.register(models.ImporterColumn, ImporterColumnAdmin) @@ -319,11 +353,14 @@ class ImporterModelAdmin(admin.ModelAdmin):      list_display = ('name', 'klass')      model = models.ImporterModel +  admin_site.register(models.ImporterModel, ImporterModelAdmin)  class FormaterTypeAdmin(admin.ModelAdmin):      list_display = ('formater_type', 'options') + +  admin_site.register(models.FormaterType, FormaterTypeAdmin) @@ -331,6 +368,8 @@ class ImportAdmin(admin.ModelAdmin):      list_display = ('name', 'importer_type', 'imported_file', 'user', 'state',                      'creation_date')      form = make_ajax_form(models.Import, {'user': 'ishtaruser'}) + +  admin_site.register(models.Import, ImportAdmin) @@ -338,6 +377,8 @@ class TargetKeyGroupAdmin(admin.ModelAdmin):      list_display = ('name', 'all_user_can_use', 'all_user_can_modify',                      'available')      search_fields = ('name',) + +  admin_site.register(models.TargetKeyGroup, TargetKeyGroupAdmin) @@ -346,6 +387,8 @@ class TargetKeyAdmin(admin.ModelAdmin):                      'value', 'is_set')      list_filter = ("is_set", "target__column__importer_type")      search_fields = ('target__target', 'value', 'key') + +  admin_site.register(models.TargetKey, TargetKeyAdmin) @@ -353,18 +396,23 @@ class OperationTypeAdmin(GeneralTypeAdmin):      list_display = GeneralTypeAdmin.list_display + ['order', 'preventive']      model = models.OperationType +  admin_site.register(models.OperationType, OperationTypeAdmin)  class SpatialReferenceSystemAdmin(GeneralTypeAdmin):      list_display = GeneralTypeAdmin.list_display + ['order', 'srid']      model = models.SpatialReferenceSystem + +  admin_site.register(models.SpatialReferenceSystem, SpatialReferenceSystemAdmin)  class ItemKeyAdmin(admin.ModelAdmin):      list_display = ('content_type', 'key', 'content_object', 'importer')      search_fields = ('key', ) + +  admin_site.register(models.ItemKey, ItemKeyAdmin) @@ -418,6 +466,83 @@ class JsonDataFieldAdmin(admin.ModelAdmin):  admin_site.register(models.JsonDataField, JsonDataFieldAdmin) +def get_choices_form(): +    cache_key, value = get_cache(models.CustomForm, ['associated-forms']) +    if value: +        return value +    forms = [] +    for slug in models.CustomForm.register(): +        forms.append((slug, models.CustomForm._register[slug].form_admin_name)) +    forms = sorted(forms, key=lambda x: x[1]) +    cache.set(cache_key, forms, settings.CACHE_TIMEOUT) +    return forms + + +class CustomFormForm(forms.ModelForm): +    class Meta: +        model = models.CustomForm +        exclude = [] +    form = forms.ChoiceField(label=_(u"Form"), choices=get_choices_form) +    users = AutoCompleteSelectMultipleField('ishtaruser', required=False, +                                            label=_(u"Users")) + + +class ExcludeFieldFormset(BaseInlineFormSet): +    def get_form_kwargs(self, index): +        kwargs = super(ExcludeFieldFormset, self).get_form_kwargs(index) +        if not self.instance or not self.instance.pk: +            return kwargs +        form = self.instance.get_form_class() +        if not form: +            kwargs['choices'] = [] +            return kwargs +        kwargs['choices'] = [('', '--')] + form.get_custom_fields() +        return kwargs + + +class ExcludeFieldForm(forms.ModelForm): +    class Meta: +        model = models.ExcludedField +        exclude = [] +    field = forms.ChoiceField(label=_(u"Field")) + +    def __init__(self, *args, **kwargs): +        choices = kwargs.pop('choices') +        super(ExcludeFieldForm, self).__init__(*args, **kwargs) +        self.fields['field'].choices = choices + + +class ExcludeFieldInline(admin.TabularInline): +    model = models.ExcludedField +    extra = 2 +    form = ExcludeFieldForm +    formset = ExcludeFieldFormset + + +class CustomFormAdmin(admin.ModelAdmin): +    list_display = ['name', 'form', 'available', 'enabled', 'apply_to_all', +                    'users_lbl', 'user_types_lbl'] +    fields = ('name', 'form', 'available', 'enabled', 'apply_to_all', 'users', +              'user_types') +    form = CustomFormForm +    inlines = [ExcludeFieldInline] + +    def get_inline_instances(self, request, obj=None): +        # no inline on creation +        if not obj: +            return [] +        return super(CustomFormAdmin, self).get_inline_instances(request, +                                                                 obj=obj) + +    def get_readonly_fields(self, request, obj=None): +        if obj: +            return ('form',) +        return [] + + +admin_site.register(models.CustomForm, CustomFormAdmin) + +  class AdministrationScriptAdmin(admin.ModelAdmin):      list_display = ['name', 'path'] @@ -426,9 +551,19 @@ class AdministrationScriptAdmin(admin.ModelAdmin):              return ('path',)          return [] +  admin_site.register(models.AdministrationScript, AdministrationScriptAdmin) +class ImageAdmin(admin.ModelAdmin): +    list_display = ('name', 'image_type', 'reference', 'internal_reference') +    list_filter = ('image_type',) +    search_fields = ('name', 'reference', 'internal_reference') + + +admin_site.register(models.IshtarImage, ImageAdmin) + +  class AdministrationTaskAdmin(admin.ModelAdmin):      readonly_fields = ('state', 'creation_date', 'launch_date',                         'finished_date', "result", ) @@ -441,6 +576,7 @@ class AdministrationTaskAdmin(admin.ModelAdmin):              return ("script", ) + self.readonly_fields          return self.readonly_fields +  admin_site.register(models.AdministrationTask, AdministrationTaskAdmin) diff --git a/ishtar_common/forms.py b/ishtar_common/forms.py index 5c3de7b77..eebd912ea 100644 --- a/ishtar_common/forms.py +++ b/ishtar_common/forms.py @@ -34,7 +34,7 @@ from django.utils.translation import ugettext_lazy as _  import models  import widgets -from wizards import MultiValueDict +from ishtar_common.utils import MultiValueDict  # from formwizard.forms import NamedUrlSessionFormWizard @@ -105,7 +105,92 @@ def get_readonly_clean(key):      return func -class FormSet(BaseFormSet): +class CustomForm(object): +    form_admin_name = "" +    form_slug = "" +    need_user_for_initialization = True + +    def __init__(self, *args, **kwargs): +        current_user = None +        if 'user' in kwargs: +            try: +                current_user = kwargs.pop('user').ishtaruser +            except AttributeError: +                pass +        super(CustomForm, self).__init__(*args, **kwargs) +        available, excluded = self.check_availability_and_excluded_fields( +            current_user) +        for exc in excluded: +            if hasattr(self, 'fields'): +                self.remove_field(exc) +            else: +                # formset +                for form in self.forms: +                    if exc in form.fields: +                        form.fields.pop(exc) + +    def are_available(self, keys): +        for k in keys: +            if k not in self.fields: +                return False +        return True + +    def remove_field(self, key): +        if key in self.fields: +            self.fields.pop(key) + +    @classmethod +    def check_availability_and_excluded_fields(cls, current_user): +        if not current_user: +            return True, [] +        base_q = {"form": cls.form_slug, 'available': True} +        # order is important : try for user, user type then all +        query_dicts = [] +        if current_user: +            dct = base_q.copy() +            dct.update({'users__pk': current_user.pk}) +            query_dicts = [dct] +            for user_type in current_user.person.person_types.all(): +                dct = base_q.copy() +                dct.update({'user_types__pk': user_type.pk}), +                query_dicts.append(dct) +        dct = base_q.copy() +        dct.update({'apply_to_all': True}) +        query_dicts.append(dct) +        excluded_lst = [] +        for query_dict in query_dicts: +            q = models.CustomForm.objects.filter(**query_dict) +            if not q.count(): +                continue +            # todo: prevent multiple result in database +            form = q.all()[0] +            if not form.enabled: +                return False, [] +            for excluded in form.excluded_fields.all(): +                # could have be filtered previously +                excluded_lst.append(excluded.field) +            break +        return True, excluded_lst + +    @classmethod +    def get_custom_fields(cls): +        if hasattr(cls, 'base_fields'): +            fields = cls.base_fields +        else: +            # formset +            fields = cls.form.base_fields +        customs = [] +        for key in fields: +            field = fields[key] +            # cannot customize display of required and hidden field +            # field with no label are also rejected +            if field.required or field.widget.is_hidden or not field.label: +                continue +            customs.append((key, field.label)) +        return sorted(customs, key=lambda x: x[1]) + + +class FormSet(CustomForm, BaseFormSet):      def __init__(self, *args, **kwargs):          self.readonly = False          if 'readonly' in kwargs: @@ -250,8 +335,31 @@ def get_data_from_formset(data):      return values +class FieldType(object): +    def __init__(self, key, model, is_multiple=False, extra_args=None): +        self.key = key +        self.model = model +        self.is_multiple = is_multiple +        self.extra_args = extra_args + +    def get_choices(self, initial=None): +        args = { +            'empty_first': not self.is_multiple, +            'initial': initial +        } +        if self.extra_args: +            args.update(self.extra_args) +        return self.model.get_types(**args) + +    def get_help(self): +        args = {} +        if self.extra_args: +            args.update(self.extra_args) +        return self.model.get_help(**args) + +  class ManageOldType(object): -    TYPES = []  # (field_name, model, is_multiple) list +    TYPES = []  # FieldType list      def __init__(self, *args, **kwargs):          """ @@ -290,12 +398,12 @@ class ManageOldType(object):                          self.init_data[k].append(val)          self.init_data = MultiValueDict(self.init_data)          super(ManageOldType, self).__init__(*args, **kwargs) -        for field_name, model, is_multiple in self.TYPES: -            self.fields[field_name].choices = \ -                model.get_types( -                    empty_first=not is_multiple, -                    initial=self.init_data.get(field_name)) -            self.fields[field_name].help_text = model.get_help() +        for field in self.TYPES: +            if field.key not in self.fields: +                continue +            self.fields[field.key].choices = field.get_choices( +                initial=self.init_data.get(field.key)) +            self.fields[field.key].help_text = field.get_help()  class DocumentGenerationForm(forms.Form): diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index 116c8c277..dadeefee0 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -35,7 +35,7 @@ import models  import widgets  from ishtar_common.templatetags.link_to_window import link_to_window  from forms import FinalForm, FormSet, reverse_lazy, name_validator, \ -    TableSelect, ManageOldType +    TableSelect, ManageOldType, CustomForm, FieldType  def get_town_field(label=_(u"Town"), required=True): @@ -731,8 +731,11 @@ class TownFormSet(FormSet):          return self.check_duplicate(('town',),                                      _("There are identical towns.")) +  TownFormset = formset_factory(TownForm, can_delete=True, formset=TownFormSet)  TownFormset.form_label = _("Towns") +TownFormset.form_admin_name = _(u"Towns") +TownFormset.form_slug = "towns"  class MergeFormSet(BaseModelFormSet): @@ -860,8 +863,11 @@ class MergeOrganizationForm(MergeForm):  ######################  # Sources management #  ###################### -class SourceForm(ManageOldType, forms.Form): +class SourceForm(CustomForm, ManageOldType, forms.Form):      form_label = _(u"Documentation informations") +    form_admin_name = _("Source - General") +    form_slug = "source-general" +      file_upload = True      associated_models = {'source_type': models.SourceType}      title = forms.CharField(label=_(u"Title"), @@ -899,10 +905,9 @@ class SourceForm(ManageOldType, forms.Form):                  'height': settings.IMAGE_MAX_SIZE[1]}),          max_length=255, required=False, widget=widgets.ImageFileInput()) -    def __init__(self, *args, **kwargs): -        super(SourceForm, self).__init__(*args, **kwargs) -        self.fields['source_type'].choices = models.SourceType.get_types( -            initial=self.init_data.get('source_type')) +    TYPES = [ +        FieldType('source_type', models.SourceType), +    ]  class SourceSelect(TableSelect): @@ -986,3 +991,5 @@ class AuthorFormSet(FormSet):  AuthorFormset = formset_factory(AuthorFormSelection, can_delete=True,                                  formset=AuthorFormSet)  AuthorFormset.form_label = _("Authors") +AuthorFormset.form_admin_name = _(u"Authors") +AuthorFormset.form_slug = "authors" diff --git a/ishtar_common/migrations/0021_auto_20171110_1717.py b/ishtar_common/migrations/0021_auto_20171110_1717.py new file mode 100644 index 000000000..c95f5e1d8 --- /dev/null +++ b/ishtar_common/migrations/0021_auto_20171110_1717.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-11-10 17:17 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ishtar_common.models +import re + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('ishtar_common', '0020_auto_20171030_1708'), +    ] + +    operations = [ +        migrations.CreateModel( +            name='ImageType', +            fields=[ +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), +                ('label', models.CharField(max_length=100, verbose_name='Label')), +                ('txt_idx', models.CharField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', max_length=100, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), +                ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), +                ('available', models.BooleanField(default=True, verbose_name='Available')), +            ], +            options={ +                'ordering': ('label',), +                'verbose_name': 'Image type', +                'verbose_name_plural': 'Image types', +            }, +            bases=(ishtar_common.models.Cached, models.Model), +        ), +        migrations.CreateModel( +            name='IshtarImage', +            fields=[ +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), +                ('image', models.ImageField(blank=True, max_length=255, null=True, upload_to=ishtar_common.models.get_image_path)), +                ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, upload_to=ishtar_common.models.get_image_path)), +                ('name', models.CharField(max_length=250, verbose_name='Name')), +                ('description', models.TextField(blank=True, null=True, verbose_name='Description')), +                ('authors_raw', models.CharField(blank=True, max_length=250, null=True, verbose_name='Authors (raw)')), +                ('creation_date', models.DateField(blank=True, null=True, verbose_name='Creation date')), +                ('reference', models.CharField(blank=True, max_length=250, null=True, verbose_name='Ref.')), +                ('internal_reference', models.CharField(blank=True, max_length=250, null=True, verbose_name='Internal ref.')), +                ('authors', models.ManyToManyField(blank=True, to='ishtar_common.Author', verbose_name='Authors')), +                ('image_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.ImageType', verbose_name='Type')), +            ], +            options={ +                'ordering': ('name',), +                'verbose_name': 'Image', +                'verbose_name_plural': 'Images', +            }, +        ), +        migrations.CreateModel( +            name='LicenseType', +            fields=[ +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), +                ('label', models.CharField(max_length=100, verbose_name='Label')), +                ('txt_idx', models.CharField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', max_length=100, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), +                ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), +                ('available', models.BooleanField(default=True, verbose_name='Available')), +            ], +            options={ +                'ordering': ('label',), +                'verbose_name': 'License type', +                'verbose_name_plural': 'License types', +            }, +            bases=(ishtar_common.models.Cached, models.Model), +        ), +        migrations.AddField( +            model_name='ishtarimage', +            name='licenses', +            field=models.ManyToManyField(blank=True, to='ishtar_common.LicenseType', verbose_name='License'), +        ), +    ] diff --git a/ishtar_common/migrations/0022_customform.py b/ishtar_common/migrations/0022_customform.py new file mode 100644 index 000000000..8eaed6d89 --- /dev/null +++ b/ishtar_common/migrations/0022_customform.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-11-17 12:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('ishtar_common', '0021_auto_20171110_1717'), +    ] + +    operations = [ +        migrations.CreateModel( +            name='CustomForm', +            fields=[ +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), +                ('name', models.CharField(max_length=250, verbose_name='Name')), +                ('form', models.CharField(max_length=250, verbose_name='Form')), +                ('available', models.BooleanField(default=True, verbose_name='Available')), +                ('apply_to_all', models.BooleanField(default=False, help_text='Apply this form to all users. If set to True, selecting user and user type is useless.', verbose_name='Apply to all')), +                ('user_types', models.ManyToManyField(blank=True, to='ishtar_common.PersonType')), +                ('users', models.ManyToManyField(blank=True, to='ishtar_common.IshtarUser')), +            ], +            options={ +                'ordering': ['name', 'form'], +                'verbose_name': 'Custom form', +                'verbose_name_plural': 'Custom forms', +            }, +        ), +    ] diff --git a/ishtar_common/migrations/0023_excludedfield.py b/ishtar_common/migrations/0023_excludedfield.py new file mode 100644 index 000000000..2573219ae --- /dev/null +++ b/ishtar_common/migrations/0023_excludedfield.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-11-17 17:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('ishtar_common', '0022_customform'), +    ] + +    operations = [ +        migrations.CreateModel( +            name='ExcludedField', +            fields=[ +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), +                ('field', models.CharField(max_length=250, verbose_name='Field')), +                ('custom_form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='excluded_fields', to='ishtar_common.CustomForm')), +            ], +            options={ +                'verbose_name': 'Custom form - excluded field', +                'verbose_name_plural': 'Custom form - excluded fields', +            }, +        ), +    ] diff --git a/ishtar_common/migrations/0024_custom_form_enabled.py b/ishtar_common/migrations/0024_custom_form_enabled.py new file mode 100644 index 000000000..92fd32f6e --- /dev/null +++ b/ishtar_common/migrations/0024_custom_form_enabled.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-11-21 09:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('ishtar_common', '0023_excludedfield'), +    ] + +    operations = [ +        migrations.AlterModelOptions( +            name='excludedfield', +            options={'verbose_name': 'Excluded field', 'verbose_name_plural': 'Excluded fields'}, +        ), +        migrations.AddField( +            model_name='customform', +            name='enabled', +            field=models.BooleanField(default=True, help_text='Disable with caution: disabling a form with mandatory fields may lead to database errors.', verbose_name='Enable this form'), +        ), +    ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 2c8240a72..aab532cfc 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -23,6 +23,7 @@ Models description  from cStringIO import StringIO  import copy  import datetime +import inspect  from PIL import Image  import logging  import os @@ -887,6 +888,7 @@ class HistoryError(Exception):      def __str__(self):          return repr(self.value) +  PRIVATE_FIELDS = ('id', 'history_modifier', 'order') @@ -1447,6 +1449,7 @@ def get_external_id(key, item):              dct[initial_key] = filtr(dct[initial_key])      return formula.format(**dct) +  CURRENCY = ((u"€", _(u"Euro")),              (u"$", _(u"US dollar")))  FIND_INDEX_SOURCE = ((u"O", _(u"Operations")), @@ -1629,6 +1632,84 @@ post_save.connect(cached_site_changed, sender=IshtarSiteProfile)  post_delete.connect(cached_site_changed, sender=IshtarSiteProfile) +class CustomForm(models.Model): +    name = models.CharField(_(u"Name"), max_length=250) +    form = models.CharField(_(u"Form"), max_length=250) +    available = models.BooleanField(_(u"Available"), default=True) +    enabled = models.BooleanField( +        _(u"Enable this form"), default=True, +        help_text=_(u"Disable with caution: disabling a form with mandatory " +                    u"fields may lead to database errors.")) +    apply_to_all = models.BooleanField( +        _(u"Apply to all"), default=False, +        help_text=_(u"Apply this form to all users. If set to True, selecting " +                    u"user and user type is useless.")) +    users = models.ManyToManyField('IshtarUser', blank=True) +    user_types = models.ManyToManyField('PersonType', blank=True) + +    class Meta: +        verbose_name = _(u"Custom form") +        verbose_name_plural = _(u"Custom forms") +        ordering = ['name', 'form'] + +    def __unicode__(self): +        return u"{} - {}".format(self.name, self.form) + +    def users_lbl(self): +        users = [unicode(user) for user in self.users.all()] +        return " ; ".join(users) + +    users_lbl.short_description = _(u"Users") + +    def user_types_lbl(self): +        user_types = [unicode(u) for u in self.user_types.all()] +        return " ; ".join(user_types) + +    user_types_lbl.short_description = _(u"User types") + +    @classmethod +    def register(cls): +        if hasattr(cls, '_register'): +            return cls._register +        cache_key, value = get_cache(cls.__class__, ['dct-forms'], +                                     app_label='ishtar_common') +        if value: +            cls._register = value +            return cls._register +        cls._register = {} +        # ideally should be improved but only used in admin +        from ishtar_common.admin import ISHTAR_FORMS +        from ishtar_common.forms import CustomForm + +        for app_form in ISHTAR_FORMS: +            for form in dir(app_form): +                if 'Form' not in form: +                    # not very clean... but do not treat inappropriate items +                    continue +                form = getattr(app_form, form) +                if not inspect.isclass(form) \ +                        or not issubclass(form, CustomForm) \ +                        or not getattr(form, 'form_slug', None): +                    continue +                cls._register[form.form_slug] = form +        return cls._register + +    def get_form_class(self): +        register = self.register() +        if self.form not in self._register: +            return +        return register[self.form] + + +class ExcludedField(models.Model): +    custom_form = models.ForeignKey(CustomForm, related_name='excluded_fields') +    field = models.CharField(_(u"Field"), max_length=250) + +    class Meta: +        verbose_name = _(u"Excluded field") +        verbose_name_plural = _(u"Excluded fields") + +  class GlobalVar(models.Model, Cached):      slug = models.SlugField(_(u"Variable name"), unique=True)      description = models.TextField(_(u"Description of the variable"), @@ -1651,6 +1732,7 @@ def cached_globalvar_changed(sender, **kwargs):      cache_key, value = get_cache(GlobalVar, var.slug)      cache.set(cache_key, var.value, settings.CACHE_TIMEOUT) +  post_save.connect(cached_globalvar_changed, sender=GlobalVar) @@ -2146,6 +2228,8 @@ class OrganizationType(GeneralType):          verbose_name = _(u"Organization type")          verbose_name_plural = _(u"Organization types")          ordering = ('label',) + +  post_save.connect(post_save_cache, sender=OrganizationType)  post_delete.connect(post_save_cache, sender=OrganizationType) @@ -2577,6 +2661,8 @@ class AuthorType(GeneralType):          verbose_name = _(u"Author type")          verbose_name_plural = _(u"Author types")          ordering = ['order', 'label'] + +  post_save.connect(post_save_cache, sender=AuthorType)  post_delete.connect(post_save_cache, sender=AuthorType) @@ -2635,6 +2721,8 @@ class Format(GeneralType):          verbose_name = _(u"Format type")          verbose_name_plural = _(u"Format types")          ordering = ['label'] + +  post_save.connect(post_save_cache, sender=Format)  post_delete.connect(post_save_cache, sender=Format) @@ -2696,6 +2784,45 @@ class Source(OwnPerms, ImageModel, models.Model):          return slugify(u"-".join(values)) +class LicenseType(GeneralType): +    class Meta: +        verbose_name = _(u"License type") +        verbose_name_plural = _(u"License types") +        ordering = ('label',) + + +class ImageType(GeneralType): +    class Meta: +        verbose_name = _(u"Image type") +        verbose_name_plural = _(u"Image types") +        ordering = ('label',) + + +class IshtarImage(ImageModel): +    name = models.CharField(_(u"Name"), max_length=250) +    description = models.TextField(_(u"Description"), blank=True, null=True) +    licenses = models.ManyToManyField(LicenseType, verbose_name=_(u"License"), +                                      blank=True) +    authors = models.ManyToManyField(Author, verbose_name=_(u"Authors"), +                                     blank=True) +    authors_raw = models.CharField(verbose_name=_(u"Authors (raw)"), +                                   blank=True, null=True, max_length=250) + +    image_type = models.ForeignKey(ImageType, verbose_name=_(u"Type"), +                                   blank=True, null=True) +    creation_date = models.DateField(blank=True, null=True, +                                     verbose_name=_(u"Creation date")) +    reference = models.CharField(_(u"Ref."), max_length=250, null=True, +                                 blank=True) +    internal_reference = models.CharField( +        _(u"Internal ref."), max_length=250, null=True, blank=True) + +    class Meta: +        verbose_name = _(u"Image") +        verbose_name_plural = _(u"Images") +        ordering = ('name',) + +  if settings.COUNTRY == 'fr':      class Arrondissement(models.Model):          name = models.CharField(u"Nom", max_length=30) @@ -2893,6 +3020,8 @@ class OperationType(GeneralType):          if not key:              return op_type.preventive          return key == op_type.txt_idx + +  post_save.connect(post_save_cache, sender=OperationType)  post_delete.connect(post_save_cache, sender=OperationType) diff --git a/ishtar_common/tests.py b/ishtar_common/tests.py index 63d80d5ab..7c8b2fd5c 100644 --- a/ishtar_common/tests.py +++ b/ishtar_common/tests.py @@ -1,6 +1,6 @@  #!/usr/bin/env python  # -*- coding: utf-8 -*- -# Copyright (C) 2015-2016 Étienne Loks  <etienne.loks_AT_peacefrogsDOTnet> +# Copyright (C) 2015-2017 É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 @@ -288,6 +288,38 @@ class WizardTest(object):              raise ValidationError(u"Errors: {} on {}.".format(                  u" ".join(errors), current_step)) +    @classmethod +    def wizard_post(cls, client, url, current_step, form_data=None, +                    follow=True): +        if not url: +            url = reverse(cls.url_name) +        data = { +            '{}{}-current_step'.format(cls.url_name, +                                       cls.wizard_name): [current_step], +        } +        if not form_data: +            form_data = [] + +        # reconstruct a POST request +        if type(form_data) in (list, tuple):  # is a formset +            for d_idx, item in enumerate(form_data): +                for k in item: +                    data['{}-{}-{}'.format( +                        current_step, d_idx, k)] = item[k] +        else: +            for k in form_data: +                data['{}-{}'.format(current_step, k)] = form_data[k] + +        try: +            response = client.post(url, data, follow=follow) +        except ValidationError as e: +            msg = u"Errors: {} on {}. On \"ManagementForm data is " \ +                  u"missing or...\" error verify the wizard_name or " \ +                  u"step name".format(u" - ".join(e.messages), +                                      current_step) +            raise ValidationError(msg) +        return response +      def test_wizard(self):          if self.pass_test():              return @@ -301,35 +333,14 @@ class WizardTest(object):                  current_step, current_form = step                  if current_step in ignored:                      continue -                data = { -                    '{}{}-current_step'.format(self.url_name, -                                               self.wizard_name): -                    [current_step], -                } - -                # reconstruct a POST request -                if current_step in form_data: -                    d = form_data[current_step] -                    if type(d) in (list, tuple):  # is a formset -                        for d_idx, item in enumerate(d): -                            for k in item: -                                data['{}-{}-{}'.format( -                                    current_step, d_idx, k)] = item[k] -                    else: -                        for k in d: -                            data['{}-{}'.format(current_step, k)] = d[k] -                  next_form_is_checked = len(self.steps) > idx + 1 and \ -                    self.steps[idx + 1][0] not in ignored -                try: -                    response = self.client.post( -                        url, data, follow=not next_form_is_checked) -                except ValidationError as e: -                    msg = u"Errors: {} on {}. On \"ManagementForm data is " \ -                          u"missing or...\" error verify the wizard_name or " \ -                          u"step name".format(u" - ".join(e.messages), -                                              current_step) -                    raise ValidationError(msg) +                                       self.steps[idx + 1][0] not in ignored +                data = [] +                if current_step in form_data: +                    data = form_data[current_step] +                response = self.wizard_post( +                    self.client, url, current_step, data, +                    not next_form_is_checked)                  self.check_response(response, current_step)                  if next_form_is_checked:                      next_form = self.steps[idx + 1][0] @@ -402,7 +413,8 @@ class AdminGenTypeTest(TestCase):      gen_models = [          models.OrganizationType, models.PersonType, models.TitleType,          models.AuthorType, models.SourceType, models.OperationType, -        models.SpatialReferenceSystem, models.Format, models.SupportType] +        models.SpatialReferenceSystem, models.Format, models.SupportType, +    ]      models_with_data = gen_models + [models.ImporterModel]      models = models_with_data      module_name = 'ishtar_common' diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 5d9e85c60..cc01f23e7 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -28,6 +28,7 @@ from django.conf import settings  from django.contrib.gis.geos import GEOSGeometry  from django.core.cache import cache  from django.core.urlresolvers import reverse +from django.utils.datastructures import MultiValueDict as BaseMultiValueDict  from django.utils.safestring import mark_safe  from django.utils.translation import ugettext_lazy as _, ugettext  from django.template.defaultfilters import slugify @@ -47,13 +48,33 @@ class BColors:      UNDERLINE = '\033[4m' +class MultiValueDict(BaseMultiValueDict): +    def get(self, *args, **kwargs): +        v = super(MultiValueDict, self).getlist(*args, **kwargs) +        if callable(v): +            v = v() +        if type(v) in (list, tuple) and len(v) > 1: +            v = ",".join(v) +        elif type(v) not in (int, unicode): +            v = super(MultiValueDict, self).get(*args, **kwargs) +        return v + +    def getlist(self, *args, **kwargs): +        lst = super(MultiValueDict, self).getlist(*args, **kwargs) +        if type(lst) not in (tuple, list): +            lst = [lst] +        return lst + +  def get_current_year():      return datetime.datetime.now().year -def get_cache(cls, extra_args=[]): +def get_cache(cls, extra_args=tuple(), app_label=None): +    if not app_label: +        app_label = cls._meta.app_label      cache_key = u"{}-{}-{}".format( -        settings.PROJECT_SLUG, cls._meta.app_label, cls.__name__) +        settings.PROJECT_SLUG, app_label, cls.__name__)      for arg in extra_args:          if not arg:              cache_key += '-0' diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index f86e03df0..e82b32671 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -34,37 +34,19 @@ from django.db.models.fields.files import FileField, ImageFieldFile  from django.db.models.fields.related import ManyToManyField  from django.db.models.fields import NOT_PROVIDED -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, Http404  from django.forms import ValidationError  from django.shortcuts import redirect, render  from django.template import loader -from django.utils.datastructures import MultiValueDict as BaseMultiValueDict  from django.utils.translation import ugettext_lazy as _  from ishtar_common import models -from ishtar_common.utils import get_all_field_names +from ishtar_common.forms import CustomForm +from ishtar_common.utils import get_all_field_names, MultiValueDict  logger = logging.getLogger(__name__) -class MultiValueDict(BaseMultiValueDict): -    def get(self, *args, **kwargs): -        v = super(MultiValueDict, self).getlist(*args, **kwargs) -        if callable(v): -            v = v() -        if type(v) in (list, tuple) and len(v) > 1: -            v = ",".join(v) -        elif type(v) not in (int, unicode): -            v = super(MultiValueDict, self).get(*args, **kwargs) -        return v - -    def getlist(self, *args, **kwargs): -        lst = super(MultiValueDict, self).getlist(*args, **kwargs) -        if type(lst) not in (tuple, list): -            lst = [lst] -        return lst - -  def check_rights(rights=[], redirect_url='/'):      """      Decorator that checks the rights to access the view. @@ -125,6 +107,19 @@ def _check_right(step, condition=True):  """ +def filter_no_fields_form(form, other_check=None): +    def func(self): +        if issubclass(form, CustomForm): +            enabled, exc = form.check_availability_and_excluded_fields( +                self.request.user.ishtaruser) +            if not enabled: +                return False +        if other_check: +            return other_check(self) +        return True +    return func + +  class Wizard(NamedUrlWizardView):      model = None      label = '' @@ -155,6 +150,19 @@ class Wizard(NamedUrlWizardView):              self.condition_dict[form_key] = cond      ''' +    @classmethod +    def get_initkwargs(cls, *args, **kwargs): +        kwargs = super(Wizard, cls).get_initkwargs(*args, **kwargs) +        # remove +        for form_key in kwargs['form_list']: +            form = kwargs['form_list'][form_key] +            other_check = None +            if form_key in kwargs['condition_dict']: +                other_check = kwargs['condition_dict'][form_key] +            kwargs['condition_dict'][form_key] = filter_no_fields_form( +                form, other_check) +        return kwargs +      def dispatch(self, request, *args, **kwargs):          self.current_right = kwargs.get('current_right', None) @@ -813,7 +821,10 @@ class Wizard(NamedUrlWizardView):              data = data.copy()              if not step:                  step = self.steps.current -            form = self.get_form_list()[step] +            try: +                form = self.get_form_list()[step] +            except KeyError: +                raise Http404()              if hasattr(form, 'management_form'):                  # manage deletion                  to_delete, not_to_delete = self.get_deleted(data.keys()) | 
