summaryrefslogtreecommitdiff
path: root/ishtar_common
diff options
context:
space:
mode:
Diffstat (limited to 'ishtar_common')
-rw-r--r--ishtar_common/admin.py137
-rw-r--r--ishtar_common/forms.py126
-rw-r--r--ishtar_common/forms_common.py19
-rw-r--r--ishtar_common/migrations/0021_auto_20171110_1717.py77
-rw-r--r--ishtar_common/migrations/0022_customform.py32
-rw-r--r--ishtar_common/migrations/0023_excludedfield.py28
-rw-r--r--ishtar_common/migrations/0024_custom_form_enabled.py24
-rw-r--r--ishtar_common/models.py129
-rw-r--r--ishtar_common/tests.py72
-rw-r--r--ishtar_common/utils.py25
-rw-r--r--ishtar_common/wizards.py55
11 files changed, 654 insertions, 70 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py
index 648e5e4b8..a0224d4cd 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,11 +258,14 @@ class AuthorAdmin(admin.ModelAdmin):
model = models.Author
form = make_ajax_form(models.Author, {'person': 'person'})
+
admin_site.register(models.Author, AuthorAdmin)
class GlobalVarAdmin(admin.ModelAdmin):
list_display = ['slug', 'description', 'value']
+
+
admin_site.register(models.GlobalVar, GlobalVarAdmin)
@@ -273,16 +298,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)
@@ -310,6 +341,8 @@ class ImporterColumnAdmin(admin.ModelAdmin):
'targets_lbl', 'duplicate_fields_lbl', 'required')
list_filter = ('importer_type',)
inlines = (ImportTargetInline, ImporterDuplicateFieldInline)
+
+
admin_site.register(models.ImporterColumn, ImporterColumnAdmin)
@@ -317,11 +350,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)
@@ -329,6 +365,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)
@@ -336,6 +374,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)
@@ -344,6 +384,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)
@@ -351,18 +393,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)
@@ -416,6 +463,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']
@@ -424,9 +548,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", )
@@ -439,6 +573,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 1394cb8eb..12dbf9069 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())