summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--archaeological_context_records/migrations/0014_contextrecord_images.py21
-rw-r--r--archaeological_context_records/models.py4
-rw-r--r--archaeological_finds/forms.py12
-rw-r--r--archaeological_finds/migrations/0014_auto_20171110_1717.py26
-rw-r--r--archaeological_finds/models_finds.py5
-rw-r--r--archaeological_finds/models_treatments.py4
-rw-r--r--archaeological_operations/forms.py166
-rw-r--r--archaeological_operations/migrations/0013_operation_images.py21
-rw-r--r--archaeological_operations/models.py4
-rw-r--r--archaeological_operations/tests.py105
-rw-r--r--archaeological_operations/views.py5
-rw-r--r--archaeological_operations/wizards.py6
-rw-r--r--ishtar_common/admin.py137
-rw-r--r--ishtar_common/forms.py126
-rw-r--r--ishtar_common/forms_common.py15
-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.py124
-rw-r--r--ishtar_common/tests.py72
-rw-r--r--ishtar_common/utils.py25
-rw-r--r--ishtar_common/wizards.py55
23 files changed, 945 insertions, 149 deletions
diff --git a/archaeological_context_records/migrations/0014_contextrecord_images.py b/archaeological_context_records/migrations/0014_contextrecord_images.py
new file mode 100644
index 000000000..641309c97
--- /dev/null
+++ b/archaeological_context_records/migrations/0014_contextrecord_images.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-11-10 17:17
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0021_auto_20171110_1717'),
+ ('archaeological_context_records', '0013_auto_20171026_1827'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='contextrecord',
+ name='images',
+ field=models.ManyToManyField(blank=True, to='ishtar_common.IshtarImage', verbose_name='Images'),
+ ),
+ ]
diff --git a/archaeological_context_records/models.py b/archaeological_context_records/models.py
index 925a48597..dde661ee7 100644
--- a/archaeological_context_records/models.py
+++ b/archaeological_context_records/models.py
@@ -33,7 +33,7 @@ from ishtar_common.utils import cached_label_changed
from ishtar_common.models import GeneralType, BaseHistorizedItem, \
HistoricalRecords, OwnPerms, ShortMenuItem, Source, GeneralRelationType,\
GeneralRecordRelations, post_delete_record_relation, \
- ImageModel, post_save_cache, ValueGetter, BulkUpdatedItem
+ ImageModel, post_save_cache, ValueGetter, BulkUpdatedItem, IshtarImage
from archaeological_operations.models import Operation, Period, Parcel
@@ -302,6 +302,8 @@ class ContextRecord(BulkUpdatedItem, BaseHistorizedItem,
point_2d = models.PointField(_(u"Point (2D)"), blank=True, null=True)
point = models.PointField(_(u"Point (3D)"), blank=True, null=True, dim=3)
polygon = models.PolygonField(_(u"Polygon"), blank=True, null=True)
+ images = models.ManyToManyField(IshtarImage, verbose_name=_(u"Images"),
+ blank=True)
cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
db_index=True)
PARENT_SEARCH_VECTORS = ['operation']
diff --git a/archaeological_finds/forms.py b/archaeological_finds/forms.py
index b88ee164e..2a895064f 100644
--- a/archaeological_finds/forms.py
+++ b/archaeological_finds/forms.py
@@ -41,7 +41,7 @@ import models
from ishtar_common.forms import FormSet, FloatField, \
get_form_selection, reverse_lazy, TableSelect, get_now, FinalForm, \
- ManageOldType
+ ManageOldType, FieldType
from ishtar_common.forms_common import get_town_field, SourceSelect
from ishtar_common.utils import convert_coordinates_to_point
@@ -316,11 +316,11 @@ class PreservationForm(ManageOldType, forms.Form):
widget=forms.Textarea)
TYPES = [
- ('conservatory_state', models.ConservatoryState, False),
- ('treatment_emergency', models.TreatmentEmergencyType, False),
- ('preservation_to_consider', models.TreatmentType, True),
- ('alteration', models.AlterationType, True),
- ('alteration_cause', models.AlterationCauseType, True),
+ FieldType('conservatory_state', models.ConservatoryState),
+ FieldType('treatment_emergency', models.TreatmentEmergencyType),
+ FieldType('preservation_to_consider', models.TreatmentType, True),
+ FieldType('alteration', models.AlterationType, True),
+ FieldType('alteration_cause', models.AlterationCauseType, True),
]
def __init__(self, *args, **kwargs):
diff --git a/archaeological_finds/migrations/0014_auto_20171110_1717.py b/archaeological_finds/migrations/0014_auto_20171110_1717.py
new file mode 100644
index 000000000..63458bd9b
--- /dev/null
+++ b/archaeological_finds/migrations/0014_auto_20171110_1717.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-11-10 17:17
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0021_auto_20171110_1717'),
+ ('archaeological_finds', '0013_auto_20171026_1828'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='find',
+ name='images',
+ field=models.ManyToManyField(blank=True, to='ishtar_common.IshtarImage', verbose_name='Images'),
+ ),
+ migrations.AddField(
+ model_name='treatment',
+ name='images',
+ field=models.ManyToManyField(blank=True, to='ishtar_common.IshtarImage', verbose_name='Images'),
+ ),
+ ]
diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py
index 65850d4dc..4c8855d1d 100644
--- a/archaeological_finds/models_finds.py
+++ b/archaeological_finds/models_finds.py
@@ -34,7 +34,7 @@ from ishtar_common.utils import cached_label_changed, post_save_point
from ishtar_common.models import GeneralType, HierarchicalType, ImageModel, \
BaseHistorizedItem, ShortMenuItem, LightHistorizedItem, \
HistoricalRecords, OwnPerms, Source, Person, Basket, post_save_cache, \
- ValueGetter, get_current_profile
+ ValueGetter, get_current_profile, IshtarImage
from archaeological_operations.models import AdministrativeAct
from archaeological_context_records.models import ContextRecord, Dating
@@ -750,7 +750,8 @@ class Find(BulkUpdatedItem, ValueGetter, BaseHistorizedItem, ImageModel,
null=True)
appraisal_date = models.DateField(_(u"Appraisal date"), blank=True,
null=True)
-
+ images = models.ManyToManyField(IshtarImage, verbose_name=_(u"Images"),
+ blank=True)
cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
db_index=True)
history = HistoricalRecords()
diff --git a/archaeological_finds/models_treatments.py b/archaeological_finds/models_treatments.py
index 03eeed452..6c173959a 100644
--- a/archaeological_finds/models_treatments.py
+++ b/archaeological_finds/models_treatments.py
@@ -30,7 +30,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext
from ishtar_common.utils import cached_label_changed, get_current_year
from ishtar_common.models import GeneralType, ImageModel, BaseHistorizedItem, \
OwnPerms, HistoricalRecords, Person, Organization, Source, \
- ValueGetter, post_save_cache, ShortMenuItem, DashboardFormItem
+ ValueGetter, post_save_cache, ShortMenuItem, DashboardFormItem, IshtarImage
from archaeological_warehouse.models import Warehouse, Container
from archaeological_finds.models_finds import Find, FindBasket, TreatmentType
from archaeological_operations.models import ClosedItem, Operation
@@ -115,6 +115,8 @@ class Treatment(DashboardFormItem, ValueGetter, BaseHistorizedItem,
blank=True, null=True)
target_is_basket = models.BooleanField(_(u"Target a basket"),
default=False)
+ images = models.ManyToManyField(IshtarImage, verbose_name=_(u"Images"),
+ blank=True)
cached_label = models.TextField(_(u"Cached name"), null=True, blank=True,
db_index=True)
history = HistoricalRecords()
diff --git a/archaeological_operations/forms.py b/archaeological_operations/forms.py
index 841131da6..6966fff50 100644
--- a/archaeological_operations/forms.py
+++ b/archaeological_operations/forms.py
@@ -49,7 +49,7 @@ from ishtar_common import widgets
from ishtar_common.forms import FinalForm, FormSet, get_now, \
reverse_lazy, get_form_selection, TableSelect, get_data_from_formset, \
- ManageOldType
+ ManageOldType, CustomForm, FieldType
from ishtar_common.forms_common import TownFormSet, SourceForm, SourceSelect, \
get_town_field
@@ -477,6 +477,8 @@ class RecordRelationsFormSetBase(FormSet):
RecordRelationsFormSet = formset_factory(
RecordRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase)
RecordRelationsFormSet.form_label = _(u"Relations")
+RecordRelationsFormSet.form_admin_name = _("Operations - Relations")
+RecordRelationsFormSet.form_slug = "operation-relations"
class OperationSelect(TableSelect):
@@ -675,12 +677,15 @@ class OperationFormFileChoice(forms.Form):
validators=[valid_id(File)], required=False)
-class OperationFormAbstract(forms.Form):
+class OperationFormAbstract(CustomForm, forms.Form):
form_label = _(u"Abstract")
+ form_admin_name = _("Operations - Abstract")
+ form_slug = "operation-abstract"
abstract = forms.CharField(
label=_(u"Abstract"),
widget=forms.Textarea(attrs={'class': 'xlarge'}), required=False)
+
SLICING = (("month", _(u"months")), ('year', _(u"years")),)
DATE_SOURCE = (('creation', _(u"Creation date")),
@@ -761,8 +766,11 @@ class DashboardForm(forms.Form):
return fltr
-class OperationFormGeneral(ManageOldType, forms.Form):
+class OperationFormGeneral(ManageOldType, CustomForm, forms.Form):
form_label = _(u"General")
+ form_admin_name = _(u"Operation - General")
+ form_slug = "operation-general"
+
file_upload = True
associated_models = {'scientist': Person,
'in_charge': Person,
@@ -882,33 +890,36 @@ class OperationFormGeneral(ManageOldType, forms.Form):
'height': settings.IMAGE_MAX_SIZE[1]}),
max_length=255, required=False, widget=widgets.ImageFileInput())
+ FILE_FIELDS = [
+ 'report_delivery_date',
+ 'report_processing',
+ 'cira_rapporteur',
+ 'cira_date',
+ 'negative_result'
+ ]
+ WAREHOUSE_FIELDS = [
+ 'documentation_deadline',
+ 'documentation_received',
+ 'finds_deadline',
+ 'finds_received',
+ ]
+ TYPES = [
+ FieldType('operation_type', models.OperationType),
+ FieldType('report_processing', models.ReportState),
+ ]
+
def __init__(self, *args, **kwargs):
super(OperationFormGeneral, self).__init__(*args, **kwargs)
profile = get_current_profile()
if not profile.files:
- self.fields.pop('report_delivery_date')
- self.fields.pop('report_processing')
- self.fields.pop('cira_rapporteur')
- self.fields.pop('cira_date')
- self.fields.pop('negative_result')
+ for key in self.FILE_FIELDS:
+ self.remove_field(key)
if not profile.warehouse:
- self.fields.pop('documentation_deadline')
- self.fields.pop('documentation_received')
- self.fields.pop('finds_deadline')
- self.fields.pop('finds_received')
- self.fields['operation_type'].choices = \
- models.OperationType.get_types(
- initial=self.init_data.get('operation_type'))
- self.fields['operation_type'].help_text = \
- models.OperationType.get_help()
- if 'report_processing' in self.fields:
- self.fields['report_processing'].choices = \
- models.ReportState.get_types(
- initial=self.init_data.get('report_processing'))
- self.fields['report_processing'].help_text = \
- models.ReportState.get_help()
- self.fields['record_quality'].choices = \
- [('', '--')] + list(models.QUALITY)
+ for key in self.WAREHOUSE_FIELDS:
+ self.remove_field(key)
+ if 'record_quality' in self.fields:
+ self.fields['record_quality'].choices = \
+ [('', '--')] + list(models.QUALITY)
if 'operation_code' in self.fields:
fields = OrderedDict()
ope_code = self.fields.pop('operation_code')
@@ -920,17 +931,20 @@ class OperationFormGeneral(ManageOldType, forms.Form):
def clean(self):
cleaned_data = self.cleaned_data
+
# verify the logic between start date and excavation end date
- if cleaned_data.get('excavation_end_date'):
+ if self.are_available(['excavation_end_date', 'start_date']) \
+ and cleaned_data.get('excavation_end_date'):
if not self.cleaned_data['start_date']:
raise forms.ValidationError(
- _(u"If you want to set an excavation end date you have to "
- u"provide a start date."))
+ _(u"If you want to set an excavation end date you "
+ u"have to provide a start date."))
if cleaned_data['excavation_end_date'] \
< cleaned_data['start_date']:
raise forms.ValidationError(
_(u"The excavation end date cannot be before the start "
u"date."))
+
# verify patriarche
code_p = self.cleaned_data.get('code_patriarche', None)
@@ -942,11 +956,13 @@ class OperationFormGeneral(ManageOldType, forms.Form):
msg = u"Ce code OA a déjà été affecté à une "\
u"autre opération"
raise forms.ValidationError(msg)
+
# manage unique operation ID
year = self.cleaned_data.get("year")
operation_code = cleaned_data.get("operation_code", None)
if not operation_code:
return self.cleaned_data
+
ops = models.Operation.objects.filter(year=year,
operation_code=operation_code)
if 'pk' in cleaned_data and cleaned_data['pk']:
@@ -989,14 +1005,18 @@ class OperationFormModifGeneral(OperationFormGeneral):
fields[key] = value
self.fields = fields
+
OperationFormModifGeneral.associated_models = \
OperationFormGeneral.associated_models.copy()
OperationFormModifGeneral.associated_models['associated_file'] = File
-class CollaboratorForm(forms.Form):
+class CollaboratorForm(CustomForm, forms.Form):
form_label = _(u"Collaborators")
+ form_admin_name = _(u"Operation - Collaborators")
+ form_slug = "operation-collaborators"
+
base_models = ['collaborator']
associated_models = {'collaborator': Person, }
collaborator = widgets.Select2MultipleField(
@@ -1004,11 +1024,15 @@ class CollaboratorForm(forms.Form):
def __init__(self, *args, **kwargs):
super(CollaboratorForm, self).__init__(*args, **kwargs)
- self.fields['collaborator'].widget.attrs['full-width'] = True
+ if 'collaborator' in self.fields:
+ self.fields['collaborator'].widget.attrs['full-width'] = True
-class OperationFormPreventive(forms.Form):
+class OperationFormPreventive(CustomForm, forms.Form):
form_label = _(u"Preventive informations - excavation")
+ form_admin_name = _(u"Operation - Preventive - Excavation")
+ form_slug = "operation-preventive-excavation"
+
cost = forms.IntegerField(label=_(u"Cost (euros)"), required=False)
scheduled_man_days = forms.IntegerField(label=_(u"Scheduled man-days"),
required=False)
@@ -1023,8 +1047,11 @@ class OperationFormPreventive(forms.Form):
validators.MaxValueValidator(100)])
-class OperationFormPreventiveDiag(forms.Form):
+class OperationFormPreventiveDiag(CustomForm, forms.Form):
form_label = _("Preventive informations - diagnostic")
+ form_admin_name = _(u"Operation - Preventive - Diagnostic")
+ form_slug = "operation-preventive-diagnostic"
+
if settings.COUNTRY == 'fr':
zoning_prescription = forms.NullBooleanField(
required=False, label=_(u"Prescription on zoning"))
@@ -1049,9 +1076,11 @@ class SelectedTownForm(forms.Form):
if towns and towns != -1:
self.fields['town'].choices = [('', '--')] + towns
+
SelectedTownFormset = formset_factory(SelectedTownForm, can_delete=True,
formset=TownFormSet)
SelectedTownFormset.form_label = _(u"Towns")
+SelectedTownFormset.form_slug = "towns"
class SelectedParcelForm(forms.Form):
@@ -1068,13 +1097,17 @@ class SelectedParcelForm(forms.Form):
if parcels:
self.fields['parcel'].choices = [('', '--')] + parcels
+
SelectedParcelFormSet = formset_factory(SelectedParcelForm, can_delete=True,
formset=ParcelFormSet)
SelectedParcelFormSet.form_label = _("Parcels")
+SelectedParcelFormSet.form_admin_name = _(u"Operations - Parcels")
+SelectedParcelFormSet.form_slug = "operation-parcels"
SelectedParcelGeneralFormSet = formset_factory(ParcelForm, can_delete=True,
formset=ParcelFormSet)
-SelectedParcelGeneralFormSet.form_label = _("Parcels")
+SelectedParcelGeneralFormSet.form_admin_name = _("Parcels")
+SelectedParcelGeneralFormSet.form_slug = "operation-parcels"
"""
class SelectedParcelFormSet(forms.Form):
@@ -1102,36 +1135,36 @@ class SelectedParcelFormSet(forms.Form):
"""
-class RemainForm(ManageOldType, forms.Form):
+class RemainForm(CustomForm, ManageOldType, forms.Form):
form_label = _("Remain types")
+ form_admin_name = _("Operations - Remains")
+ form_slug = "operation-remains"
+
base_model = 'remain'
associated_models = {'remain': models.RemainType}
remain = forms.MultipleChoiceField(
label=_("Remain type"), required=False, choices=[],
widget=forms.CheckboxSelectMultiple)
- def __init__(self, *args, **kwargs):
- super(RemainForm, self).__init__(*args, **kwargs)
- self.fields['remain'].choices = models.RemainType.get_types(
- initial=self.init_data.get('remain'),
- empty_first=False)
- self.fields['remain'].help_text = models.RemainType.get_help()
+ TYPES = [
+ FieldType('remain', models.RemainType, True),
+ ]
-class PeriodForm(ManageOldType, forms.Form):
+class PeriodForm(CustomForm, ManageOldType, forms.Form):
form_label = _("Periods")
+ form_admin_name = _("Operations - Periods")
+ form_slug = "operation-periods"
+
base_model = 'period'
associated_models = {'period': models.Period}
period = forms.MultipleChoiceField(
label=_("Period"), required=False, choices=[],
widget=forms.CheckboxSelectMultiple)
- def __init__(self, *args, **kwargs):
- super(PeriodForm, self).__init__(*args, **kwargs)
- self.fields['period'].choices = models.Period.get_types(
- initial=self.init_data.get('period'),
- empty_first=False)
- self.fields['period'].help_text = models.Period.get_help()
+ TYPES = [
+ FieldType('period', models.Period, True),
+ ]
class ArchaeologicalSiteForm(ManageOldType, forms.Form):
@@ -1144,21 +1177,16 @@ class ArchaeologicalSiteForm(ManageOldType, forms.Form):
label=_("Remains"), choices=[], widget=widgets.Select2Multiple,
required=False)
+ TYPES = [
+ FieldType('periods', models.Period, True),
+ FieldType('remains', models.RemainType, True),
+ ]
+
def __init__(self, *args, **kwargs):
self.limits = {}
if 'limits' in kwargs:
kwargs.pop('limits')
super(ArchaeologicalSiteForm, self).__init__(*args, **kwargs)
- self.fields['periods'].choices = \
- models.Period.get_types(
- empty_first=False,
- initial=self.init_data.get('periods'))
- self.fields['periods'].help_text = models.Period.get_help()
- self.fields['remains'].choices = \
- models.RemainType.get_types(
- initial=self.init_data.get('remains'),
- empty_first=False)
- self.fields['remains'].help_text = models.RemainType.get_help()
def clean_reference(self):
reference = self.cleaned_data['reference']
@@ -1197,6 +1225,9 @@ class ArchaeologicalSiteBasicForm(forms.Form):
ArchaeologicalSiteFormSet = formset_factory(ArchaeologicalSiteBasicForm,
can_delete=True, formset=FormSet)
ArchaeologicalSiteFormSet.form_label = _("Archaeological sites")
+ArchaeologicalSiteFormSet.form_admin_name = _("Operation - Archaeological "
+ "sites")
+ArchaeologicalSiteFormSet.form_slug = "operation-archaeological-sites"
class ArchaeologicalSiteSelectionForm(forms.Form):
@@ -1224,6 +1255,9 @@ class OperationDeletionForm(FinalForm):
class OperationSourceForm(SourceForm):
+ form_admin_name = _("Operation Sources - Main")
+ form_slug = "operation-source-relations"
+
pk = forms.IntegerField(required=False, widget=forms.HiddenInput)
index = forms.IntegerField(label=_(u"Index"))
hidden_operation_id = forms.IntegerField(label="",
@@ -1371,8 +1405,11 @@ class AdministrativeActOpeFormSelection(forms.Form):
return cleaned_data
-class AdministrativeActOpeForm(ManageOldType, forms.Form):
+class AdministrativeActOpeForm(CustomForm, ManageOldType, forms.Form):
form_label = _("General")
+ form_admin_name = _("Operations - Administrative act - General")
+ form_slug = "operation-adminact-general"
+
associated_models = {'act_type': models.ActType, }
# 'signatory':Person}
act_type = forms.ChoiceField(label=_("Act type"), choices=[])
@@ -1388,13 +1425,10 @@ class AdministrativeActOpeForm(ManageOldType, forms.Form):
ref_sra = forms.CharField(label=u"Autre référence", max_length=15,
required=False)
- def __init__(self, *args, **kwargs):
- super(AdministrativeActOpeForm, self).__init__(*args, **kwargs)
- self.fields['act_type'].choices = models.ActType.get_types(
- initial=self.init_data.get('act_type'),
- dct={'intented_to': 'O'})
- self.fields['act_type'].help_text = models.ActType.get_help(
- dct={'intented_to': 'O'})
+ TYPES = [
+ FieldType('act_type', models.ActType,
+ extra_args={"dct": {'intented_to': 'O'}}),
+ ]
class AdministrativeActModifForm(object):
@@ -1410,7 +1444,7 @@ class AdministrativeActModifForm(object):
def clean(self):
# manage unique act ID
- year = self.cleaned_data.get("signature_date")
+ year = self.cleaned_data.get("signature_date", None)
if not year or not hasattr(year, 'year'):
return self.cleaned_data
year = year.year
diff --git a/archaeological_operations/migrations/0013_operation_images.py b/archaeological_operations/migrations/0013_operation_images.py
new file mode 100644
index 000000000..e32d9371f
--- /dev/null
+++ b/archaeological_operations/migrations/0013_operation_images.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2017-11-10 17:17
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0021_auto_20171110_1717'),
+ ('archaeological_operations', '0012_auto_20171026_1827'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='operation',
+ name='images',
+ field=models.ManyToManyField(blank=True, to='ishtar_common.IshtarImage', verbose_name='Images'),
+ ),
+ ]
diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py
index cf648a43a..2f1ea3ea3 100644
--- a/archaeological_operations/models.py
+++ b/archaeological_operations/models.py
@@ -38,7 +38,7 @@ from ishtar_common.models import GeneralType, BaseHistorizedItem, \
SourceType, Person, Organization, Town, Dashboard, IshtarUser, ValueGetter,\
DocumentTemplate, ShortMenuItem, DashboardFormItem, GeneralRelationType,\
GeneralRecordRelations, post_delete_record_relation, OperationType, \
- ImageModel, post_save_cache, PersonType
+ ImageModel, post_save_cache, PersonType, IshtarImage
class RemainType(GeneralType):
@@ -352,6 +352,8 @@ class Operation(ClosedItem, BaseHistorizedItem, ImageModel, OwnPerms,
comment = models.TextField(_(u"General comment"), null=True, blank=True)
scientific_documentation_comment = models.TextField(
_(u"Comment about scientific documentation"), null=True, blank=True)
+ images = models.ManyToManyField(IshtarImage, verbose_name=_(u"Images"),
+ blank=True)
cached_label = models.CharField(_(u"Cached name"), max_length=500,
null=True, blank=True, db_index=True)
archaeological_sites = models.ManyToManyField(
diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py
index ec7ae44c5..af6199774 100644
--- a/archaeological_operations/tests.py
+++ b/archaeological_operations/tests.py
@@ -40,7 +40,7 @@ from ishtar_common.models import OrganizationType, Organization, ItemKey, \
ImporterType, IshtarUser, TargetKey, ImporterModel, IshtarSiteProfile, \
Town, ImporterColumn, Person, Author, SourceType, AuthorType, \
DocumentTemplate, PersonType, TargetKeyGroup, JsonDataField, \
- JsonDataSection, ImportTarget, FormaterType
+ JsonDataSection, ImportTarget, FormaterType, CustomForm, ExcludedField
from archaeological_files.models import File, FileType
from archaeological_context_records.models import Unit
@@ -1142,6 +1142,109 @@ class OperationTest(TestCase, OperationInitTest):
self.assertNotIn(u"Marmotte".encode('utf-8'), response.content)
+class CustomFormTest(TestCase, OperationInitTest):
+ fixtures = FILE_FIXTURES
+
+ def setUp(self):
+ IshtarSiteProfile.objects.get_or_create(
+ slug='default', active=True)
+ self.username, self.password, self.user = create_superuser()
+ self.alt_username, self.alt_password, self.alt_user = create_user()
+ self.alt_user.user_permissions.add(Permission.objects.get(
+ codename='view_own_operation'))
+ self.orgas = self.create_orgas(self.user)
+ self.operations = self.create_operation(self.user, self.orgas[0])
+ self.operations += self.create_operation(self.alt_user, self.orgas[0])
+ self.item = self.operations[0]
+
+ def test_filters(self):
+ c = Client()
+ c.login(username=self.username, password=self.password)
+
+ cls_wiz = OperationWizardModifTest
+ url = reverse(cls_wiz.url_name)
+ # first wizard step
+ step = 'selec-operation_modification'
+ cls_wiz.wizard_post(c, url, step, {'pk': self.operations[0].pk})
+
+ step = 'general-operation_modification'
+ data = {
+ '{}{}-current_step'.format(cls_wiz.url_name,
+ cls_wiz.wizard_name): [step],
+ }
+ key_in_charge = "in_charge"
+ response = c.post(url, data)
+ self.assertIn(
+ key_in_charge, response.content,
+ msg="filter all - 'in charge' field not found on the modification "
+ "wizard")
+ f = CustomForm.objects.create(name="Test", form="operation-general",
+ available=True, apply_to_all=True)
+ ExcludedField.objects.create(custom_form=f, field="in_charge")
+
+ response = c.post(url, data)
+ self.assertNotIn(
+ key_in_charge, response.content,
+ msg="filter all - 'in charge' field found on the modification "
+ "wizard. It should have been filtered.")
+
+ # user type form prevail on "all"
+ f_scientist = CustomForm.objects.create(
+ name="Test", form="operation-general", available=True)
+ tpe = PersonType.objects.get(txt_idx='head_scientist')
+ key_address = "address"
+ f_scientist.user_types.add(tpe)
+ self.user.ishtaruser.person.person_types.add(tpe)
+ ExcludedField.objects.create(custom_form=f_scientist, field="address")
+ response = c.post(url, data)
+ self.assertIn(
+ key_in_charge, response.content,
+ msg="filter user type - 'in charge' field not found on the "
+ "modification wizard. It should not have been filtered.")
+ self.assertNotIn(
+ key_address, response.content,
+ msg="filter user type - 'address' field found on the "
+ "modification wizard. It should have been filtered.")
+
+ # user prevail on "all" and "user_types"
+ f_user = CustomForm.objects.create(
+ name="Test", form="operation-general", available=True)
+ f_user.users.add(self.user.ishtaruser)
+ self.user.ishtaruser.person.person_types.add(tpe)
+ response = c.post(url, data)
+ self.assertIn(
+ key_in_charge, response.content,
+ msg="filter user - 'in charge' field not found on the modification "
+ "wizard. It should not have been filtered.")
+ self.assertIn(
+ key_address, response.content,
+ msg="filter user - 'address' field not found on the modification "
+ "wizard. It should not have been filtered.")
+
+ def test_enabled(self):
+ c = Client()
+ c.login(username=self.username, password=self.password)
+
+ cls_wiz = OperationWizardModifTest
+ url = reverse(cls_wiz.url_name)
+ # first wizard step
+ step = 'selec-operation_modification'
+ cls_wiz.wizard_post(c, url, step, {'pk': self.operations[0].pk})
+
+ step = 'collaborators-operation_modification'
+ data = {
+ '{}{}-current_step'.format(cls_wiz.url_name,
+ cls_wiz.wizard_name): [step],
+ }
+ response = c.post(url, data)
+ self.assertNotEqual(response.status_code, 404)
+ CustomForm.objects.create(
+ name="Test2", form="operation-collaborators", available=True,
+ apply_to_all=True, enabled=False)
+ response = c.post(url, data)
+ self.assertEqual(response.status_code, 404)
+
+
class OperationSearchTest(TestCase, OperationInitTest):
fixtures = FILE_FIXTURES
diff --git a/archaeological_operations/views.py b/archaeological_operations/views.py
index 98da31801..f295e0f9d 100644
--- a/archaeological_operations/views.py
+++ b/archaeological_operations/views.py
@@ -82,6 +82,7 @@ def autocomplete_archaeologicalsite(request):
for site in sites])
return HttpResponse(data, content_type='text/plain')
+
new_archaeologicalsite = new_item(models.ArchaeologicalSite,
ArchaeologicalSiteForm, many=True)
@@ -132,6 +133,7 @@ def get_available_operation_code(request, year=None):
models.Operation.get_available_operation_code(year)})
return HttpResponse(data, content_type='text/plain')
+
get_operation = get_item(models.Operation, 'get_operation', 'operation')
show_operation = show_item(models.Operation, 'operation')
@@ -158,11 +160,13 @@ def dashboard_operation(request, *args, **kwargs):
dct = {'dashboard': models.OperationDashboard()}
return render(request, 'ishtar/dashboards/dashboard_operation.html', dct)
+
operation_search_wizard = SearchWizard.as_view(
[('general-operation_search', OperationFormSelection)],
label=_(u"Operation search"),
url_name='operation_search',)
+
wizard_steps = [
('filechoice-operation_creation', OperationFormFileChoice),
('general-operation_creation', OperationFormGeneral),
@@ -191,6 +195,7 @@ def get_check_files_for_operation(other_check=None):
return other_check(self)
return func
+
check_files_for_operation = get_check_files_for_operation()
diff --git a/archaeological_operations/wizards.py b/archaeological_operations/wizards.py
index fffe34ca7..24c1af45b 100644
--- a/archaeological_operations/wizards.py
+++ b/archaeological_operations/wizards.py
@@ -23,6 +23,7 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.db.models import Max
+from django.http import Http404
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
@@ -149,7 +150,10 @@ class OperationWizard(Wizard):
data = {}
if not step:
step = self.steps.current
- form = self.get_form_list()[step]
+ try:
+ form = self.get_form_list()[step]
+ except KeyError:
+ raise Http404()
# manage the dynamic choice of towns
if step.startswith('towns') and hasattr(form, 'management_form'):
data['TOWNS'] = self.get_towns()
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py
index d3292148f..189a02c05 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,22 @@ 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_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_forms, operation_forms,
+ context_record_forms, find_forms, treatment_forms,
+ warehouse_forms]
class ImportGenericForm(forms.Form):
@@ -76,6 +92,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 +168,7 @@ class IshtarSiteProfileAdmin(admin.ModelAdmin):
'find', 'warehouse', 'mapping', 'preservation')
model = models.IshtarSiteProfile
+
admin_site.register(models.IshtarSiteProfile, IshtarSiteProfileAdmin)
@@ -158,6 +176,7 @@ class DepartmentAdmin(admin.ModelAdmin):
list_display = ('number', 'label',)
model = models.Department
+
admin_site.register(models.Department, DepartmentAdmin)
@@ -168,6 +187,7 @@ class OrganizationAdmin(HistorizedObjectAdmin):
exclude = ('merge_key', 'merge_exclusion', 'merge_candidate', )
model = models.Organization
+
admin_site.register(models.Organization, OrganizationAdmin)
@@ -179,6 +199,7 @@ class PersonAdmin(HistorizedObjectAdmin):
form = make_ajax_form(models.Person, {'attached_to': 'organization'})
model = models.Person
+
admin_site.register(models.Person, PersonAdmin)
@@ -236,6 +257,7 @@ class AuthorAdmin(admin.ModelAdmin):
model = models.Author
form = make_ajax_form(models.Author, {'person': 'person'})
+
admin_site.register(models.Author, AuthorAdmin)
@@ -244,11 +266,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 +300,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 +343,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 +352,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 +367,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 +376,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 +386,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 +395,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 +465,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 +550,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 +575,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..32c91b683 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):
@@ -733,6 +733,8 @@ class TownFormSet(FormSet):
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,7 +862,7 @@ class MergeOrganizationForm(MergeForm):
######################
# Sources management #
######################
-class SourceForm(ManageOldType, forms.Form):
+class SourceForm(CustomForm, ManageOldType, forms.Form):
form_label = _(u"Documentation informations")
file_upload = True
associated_models = {'source_type': models.SourceType}
@@ -899,10 +901,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 +987,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 09aec0cdb..b0751f661 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -887,6 +887,7 @@ class HistoryError(Exception):
def __str__(self):
return repr(self.value)
+
PRIVATE_FIELDS = ('id', 'history_modifier', 'order')
@@ -1447,6 +1448,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 +1631,80 @@ 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 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 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 +1727,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 +2223,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 +2656,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 +2716,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 +2779,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 +3015,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())