#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2016 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. """ Administrative forms definitions: manage accounts and persons """ import requests import tempfile from django import forms from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.core import validators from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist from django.core.files import File from django.forms.formsets import formset_factory from django.forms.models import BaseModelFormSet, BaseFormSet from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _, pgettext from . import models from . import widgets from bootstrap_datepicker.widgets import DatePicker from ishtar_common.templatetags.link_to_window import simple_link_to_window from .forms import FinalForm, FormSet, reverse_lazy, name_validator, \ TableSelect, ManageOldType, CustomForm, FieldType, FormHeader, \ FormSetWithDeleteSwitches, BSForm, get_data_from_formset, \ file_size_validator, HistorySelect, CustomFormSearch, QAForm, IshtarForm from ishtar_common.utils import is_downloadable, clean_session_cache, \ max_size_help from archaeological_operations.models import Operation from archaeological_context_records.models import ContextRecord from archaeological_finds.models import Find from archaeological_warehouse.models import Container def get_town_field(label=_(u"Town"), required=True): help_text = _( u"

Type name, department code of the " u"town you would like to select. The search is insensitive to case." u"

\n

Only the first twenty results are displayed but specifying " u"the department code is generally sufficient to get the appropriate " u"result.

\n

For instance type \"saint denis 93\"" u" for getting the french town Saint-Denis in the Seine-Saint-Denis " u"department.

") # !FIXME hard_link, reverse_lazy doen't seem to work with formsets return forms.IntegerField( widget=widgets.JQueryAutoComplete( "/" + settings.URL_PATH + 'autocomplete-town', associated_model=models.Town), validators=[models.valid_id(models.Town)], label=label, help_text=mark_safe(help_text), required=required) def get_advanced_town_field(label=_(u"Town"), required=True): # !FIXME hard_link, reverse_lazy doen't seem to work with formsets return forms.IntegerField( widget=widgets.JQueryTown( "/" + settings.URL_PATH + 'autocomplete-advanced-town'), validators=[models.valid_id(models.Town)], label=label, required=required) def get_person_field(label=_(u"Person"), required=True, person_types=[]): # !FIXME hard_link, reverse_lazy doen't seem to work with formsets widget = None url = "/" + settings.URL_PATH + 'autocomplete-person' if person_types: person_types = [ str(models.PersonType.objects.get(txt_idx=person_type).pk) for person_type in person_types] url += u"/" + u'_'.join(person_types) widget = widgets.JQueryAutoComplete(url, associated_model=models.Person) return forms.IntegerField(widget=widget, label=label, required=required, validators=[models.valid_id(models.Person)]) class NewItemForm(forms.Form): def __init__(self, *args, **kwargs): self.limits = {} if 'limits' in kwargs: limits = kwargs.pop('limits') if limits: for item in limits.split(';'): key, values = item.split('__') self.limits[key] = values.split('-') super(NewItemForm, self).__init__(*args, **kwargs) def limit_fields(self): for key in self.limits: if key in self.fields and hasattr(self.fields[key], 'choices'): new_choices = [] for value, lbl in self.fields[key].choices: if str(value) in self.limits[key]: new_choices.append((value, lbl)) self.fields[key].choices = new_choices if len(new_choices) == 1: self.fields[key].initial = [new_choices[0][0]] class NewImportForm(BSForm, forms.ModelForm): error_css_class = 'error' required_css_class = 'required' imported_images_link = forms.URLField( label=_(u"Associated images (web link to a zip file)"), required=False ) class Meta: model = models.Import fields = ( 'name', 'importer_type', 'imported_file', 'encoding', 'csv_sep', 'imported_images', 'imported_images_link', 'associated_group', 'conservative_import', 'skip_lines' ) def __init__(self, *args, **kwargs): user = kwargs.pop('user') super(NewImportForm, self).__init__(*args, **kwargs) groups = models.TargetKeyGroup.objects.filter(available=True) if not user.is_superuser: groups = groups.filter(all_user_can_use=True) if not groups.count(): self.fields.pop('associated_group') else: self.fields['associated_group'].choices = [(None, '--')] + \ [(g.pk, str(g)) for g in groups.all()] self.fields['importer_type'].choices = [('', '--')] + [ (imp.pk, imp.name) for imp in models.ImporterType.objects.filter( available=True ) ] self.fields['imported_file'].validators = [file_size_validator] self.fields['imported_images'].validators = [file_size_validator] self._post_init() def clean(self): data = self.cleaned_data if data.get('conservative_import', None) \ and data.get('importer_type') \ and not data.get('importer_type').unicity_keys: raise forms.ValidationError( _(u"This import type have no unicity type defined. " u"Conservative import is not possible.")) if data.get('imported_images_link', None) \ and data.get('imported_images', None): raise forms.ValidationError( _(u"You put either a file or a download link for images " u"but not both.")) return data def clean_imported_images_link(self): value = self.cleaned_data.get('imported_images_link', None) if value: try: assert is_downloadable(value) except (AssertionError, requests.exceptions.RequestException): raise forms.ValidationError( _(u"Invalid link or no file is available for this link.")) return value def save(self, user, commit=True): self.instance.user = user imported_images_link = self.cleaned_data.pop('imported_images_link') \ if 'imported_images_link' in self.cleaned_data else None item = super(NewImportForm, self).save(commit) if not imported_images_link: return item request = requests.get(imported_images_link, stream=True) ntf = tempfile.NamedTemporaryFile() for block in request.iter_content(1024 * 8): if not block: break ntf.write(block) file_name = imported_images_link.split('/')[-1] item.imported_images.save(file_name, File(ntf)) return item class TargetKeyForm(forms.ModelForm): class Meta: model = models.TargetKey fields = ('target', 'key', 'value') widgets = { 'key': forms.TextInput(attrs={'readonly': 'readonly'}), } target = widgets.SelectReadonlyField( model=models.ImportTarget, label=_(u"Target")) value = widgets.Select2SimpleField( label=_(u"Value"), required=False ) remember = forms.ChoiceField(label=_(u"Remember"), choices=[], required=False) NULL_VALUE = '' def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') super(TargetKeyForm, self).__init__(*args, **kwargs) instance = getattr(self, 'instance', None) self.associated_import = None if instance and instance.pk: self.associated_import = instance.associated_import self.fields['target'].choices = [(instance.target.pk, instance.target.verbose_name)] self.fields['key'].widget.attrs['readonly'] = True self.fields['key'].widget.attrs['title'] = str(instance) self.fields['value'].choices = list( instance.target.get_choices()) self.fields['value'].choices.insert( 1, (self.NULL_VALUE, _(u"Set to NULL"))) self.fields['key'].required = False self.fields['target'].required = False self.fields['value'].required = False choices = [ ('import', _(u"this import only")), ('me', _(u"me")), ] if self.associated_import and self.associated_import.associated_group \ and (self.associated_import.associated_group.all_user_can_modify or self.user.is_superuser): choices += [ ('group', str(_(u"the current group: {}")).format( self.associated_import.associated_group))] if self.user.is_superuser: choices += [('all', _("all users"))] self.fields['remember'].choices = choices self.fields['remember'].widget.attrs['class'] = 'auto' self.remember_choices = choices def clean_target(self): instance = getattr(self, 'instance', None) if instance and instance.pk: return instance.target else: return self.cleaned_data['target'] def clean_key(self): instance = getattr(self, 'instance', None) if instance and instance.pk: return instance.key else: return self.cleaned_data['key'] def save(self, commit=True): super(TargetKeyForm, self).save(commit) if not self.cleaned_data.get('value') or not self.user: return if self.cleaned_data['value'] == self.NULL_VALUE: self.instance.value = None self.instance.is_set = True can_edit_group = \ self.associated_import and \ self.associated_import.associated_group and \ (self.associated_import.associated_group.all_user_can_modify or self.user.is_superuser) remember = self.cleaned_data.get('remember') if remember == 'import' and self.associated_import: self.instance.associated_import = self.associated_import self.instance.associated_user = None self.instance.associated_group = None elif remember == 'group' and can_edit_group: self.instance.associated_import = None self.instance.associated_user = None self.instance.associated_group = \ self.associated_import.associated_group elif remember == 'all' and self.user.is_superuser: self.instance.associated_import = None self.instance.associated_user = None self.instance.associated_group = None else: # for me! self.instance.associated_import = None self.instance.associated_user = self.user.ishtaruser self.instance.associated_group = None self.instance.save() class TargetKeyFormset(BaseModelFormSet): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') super(TargetKeyFormset, self).__init__(*args, **kwargs) def get_form_kwargs(self, index): kwargs = super(TargetKeyFormset, self).get_form_kwargs(index) kwargs['user'] = self.user return kwargs class OrganizationForm(ManageOldType, NewItemForm): form_label = _(u"Organization") associated_models = {'organization_type': models.OrganizationType, "precise_town": models.Town} name = forms.CharField( label=_(u"Name"), max_length=300, validators=[name_validator]) organization_type = forms.ChoiceField(label=_(u"Organization type"), choices=[]) address = forms.CharField(label=_(u"Address"), widget=forms.Textarea, required=False) address_complement = forms.CharField(label=_(u"Address complement"), widget=forms.Textarea, required=False) postal_code = forms.CharField(label=_(u"Postal code"), max_length=10, required=False) town = forms.CharField(label=_("Town (freeform)"), max_length=30, required=False) precise_town = get_town_field(required=False) country = forms.CharField(label=_(u"Country"), max_length=30, required=False) email = forms.EmailField(label=_(u"Email"), required=False) phone = forms.CharField(label=_(u"Phone"), max_length=18, required=False) mobile_phone = forms.CharField(label=_(u"Mobile phone"), max_length=18, required=False) def __init__(self, *args, **kwargs): super(OrganizationForm, self).__init__(*args, **kwargs) self.fields['organization_type'].choices = \ models.OrganizationType.get_types( initial=self.init_data.get('organization_type')) self.fields['organization_type'].help_text = \ models.OrganizationType.get_help() self.limit_fields() def save(self, user): dct = self.cleaned_data dct['history_modifier'] = user dct['organization_type'] = models.OrganizationType.objects.get( pk=dct['organization_type']) new_item = models.Organization(**dct) new_item.save() return new_item class OrganizationSelect(TableSelect): _model = models.Organization search_vector = forms.CharField( label=_(u"Full text search"), widget=widgets.SearchWidget( 'ishtar-common', 'organization' )) name = forms.CharField(label=_(u"Name"), max_length=300) organization_type = forms.ChoiceField(label=_(u"Type"), choices=[]) def __init__(self, *args, **kwargs): super(OrganizationSelect, self).__init__(*args, **kwargs) self.fields['organization_type'].choices = \ models.OrganizationType.get_types() class OrganizationFormSelection(forms.Form): SEARCH_AND_SELECT = True form_label = _(u"Organization search") associated_models = {'pk': models.Organization} currents = {'pk': models.Organization} pk = forms.IntegerField( label="", widget=widgets.DataTable( reverse_lazy('get-organization'), OrganizationSelect, models.Organization, source_full=reverse_lazy('get-organization-full')), validators=[models.valid_id(models.Organization)]) class ManualMerge(object): def clean_to_merge(self): value = self.cleaned_data.get('to_merge', None) or [] if value: value = value.split(',') values = [] for val in value: try: values.append(int(val)) except ValueError: pass if len(values) < 2: raise forms.ValidationError(_(u"At least two items have to be " u"selected.")) self.cleaned_data['to_merge'] = values return values def get_items(self): items = [] model = self.associated_models['to_merge'] for pk in sorted(self.cleaned_data['to_merge']): try: items.append(model.objects.get(pk=pk)) except model.DoesNotExist: pass return items class MergeIntoForm(forms.Form): main_item = forms.ChoiceField( label=_("Merge all items into"), choices=[], widget=forms.RadioSelect()) def __init__(self, *args, **kwargs): self.items = kwargs.pop('items') super(MergeIntoForm, self).__init__(*args, **kwargs) self.fields['main_item'].choices = [] for pk in self.items: try: item = self.associated_model.objects.get(pk=pk) except self.associated_model.DoesNotExist: continue self.fields['main_item'].choices.append( (item.pk, mark_safe(u"{} {}".format(simple_link_to_window(item), str(item))))) def merge(self): model = self.associated_model try: main_item = model.objects.get(pk=self.cleaned_data['main_item']) except model.DoesNotExist: return for pk in self.items: if pk == str(main_item.pk): continue try: item = model.objects.get(pk=pk) except model.DoesNotExist: continue main_item.merge(item) return main_item class OrgaMergeFormSelection(ManualMerge, forms.Form): SEARCH_AND_SELECT = True form_label = _(u"Organization to merge") associated_models = {'to_merge': models.Organization} currents = {'to_merge': models.Organization} to_merge = forms.CharField( label="", required=False, widget=widgets.DataTable( reverse_lazy('get-organization'), OrganizationSelect, models.Organization, multiple_select=True, source_full=reverse_lazy('get-organization-full')),) class OrgaMergeIntoForm(MergeIntoForm): associated_model = models.Organization class BaseOrganizationForm(forms.ModelForm): form_prefix = "orga" class Meta: model = models.Organization fields = ['name', 'organization_type', 'address', 'address_complement', 'town', 'postal_code'] class PersonSelect(CustomForm, TableSelect): _model = models.Person search_vector = forms.CharField( label=_(u"Full text search"), widget=widgets.SearchWidget( 'ishtar-common', 'person' )) name = forms.CharField(label=_(u"Name"), max_length=200) surname = forms.CharField(label=_(u"Surname"), max_length=50) email = forms.CharField(label=_(u"Email"), max_length=75) person_types = forms.ChoiceField(label=_(u"Type"), choices=[]) attached_to = forms.IntegerField( label=_("Organization"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-organization'), associated_model=models.Organization), validators=[models.valid_id(models.Organization)]) def __init__(self, *args, **kwargs): super(PersonSelect, self).__init__(*args, **kwargs) self.fields['person_types'].choices = models.PersonType.get_types() class PersonFormSelection(CustomFormSearch): SEARCH_AND_SELECT = True form_label = _(u"Person search") associated_models = {'pk': models.Person} currents = {'pk': models.Person} pk = forms.IntegerField( label="", widget=widgets.DataTable( reverse_lazy('get-person'), PersonSelect, models.Person, source_full=reverse_lazy('get-person-full')), validators=[models.valid_id(models.Person)]) class QAPersonFormMulti(QAForm): form_admin_name = _(u"Person - Quick action - Modify") form_slug = "person-quickaction-modify" base_models = ['qa_title'] associated_models = { 'qa_title': models.TitleType, } MULTI = True REPLACE_FIELDS = [ 'qa_title', ] qa_title = forms.ChoiceField( label=_(u"Title"), required=False ) TYPES = [ FieldType('qa_title', models.TitleType), ] class PersonMergeFormSelection(ManualMerge, forms.Form): SEARCH_AND_SELECT = True form_label = _("Person to merge") associated_models = {'to_merge': models.Person} currents = {'to_merge': models.Person} to_merge = forms.CharField( label="", required=False, widget=widgets.DataTable( reverse_lazy('get-person'), PersonSelect, models.Person, multiple_select=True, source_full=reverse_lazy('get-person-full')),) class PersonMergeIntoForm(MergeIntoForm): associated_model = models.Person class SimplePersonForm(ManageOldType, NewItemForm): extra_form_modals = ["organization"] form_label = _("Identity") associated_models = {'attached_to': models.Organization, 'title': models.TitleType, "precise_town": models.Town} title = forms.ChoiceField(label=_("Title"), choices=[], required=False) salutation = forms.CharField(label=_("Salutation"), max_length=200, required=False) surname = forms.CharField(label=_(u"Surname"), max_length=50, validators=[name_validator]) name = forms.CharField(label=_(u"Name"), max_length=200, validators=[name_validator]) raw_name = forms.CharField(label=_(u"Raw name"), max_length=300, required=False) email = forms.EmailField(label=_(u"Email"), required=False) phone_desc = forms.CharField(label=_(u"Phone description"), max_length=300, required=False) phone = forms.CharField(label=_(u"Phone"), max_length=18, required=False) phone_desc2 = forms.CharField(label=_(u"Phone description 2"), max_length=300, required=False) phone2 = forms.CharField(label=_(u"Phone 2"), max_length=18, required=False) phone_desc3 = forms.CharField(label=_(u"Phone description 3"), max_length=300, required=False) phone3 = forms.CharField(label=_(u"Phone 3"), max_length=18, required=False) mobile_phone = forms.CharField(label=_(u"Mobile phone"), max_length=18, required=False) attached_to = forms.IntegerField( label=_("Current organization"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-organization'), associated_model=models.Organization, new=True), validators=[models.valid_id(models.Organization)], required=False) address = forms.CharField(label=_(u"Address"), widget=forms.Textarea, required=False) address_complement = forms.CharField( label=_(u"Address complement"), widget=forms.Textarea, required=False) postal_code = forms.CharField(label=_(u"Postal code"), max_length=10, required=False) town = forms.CharField(label=_("Town (freeform)"), max_length=30, required=False) precise_town = get_town_field(required=False) country = forms.CharField(label=_(u"Country"), max_length=30, required=False) alt_address = forms.CharField(label=_(u"Other address: address"), widget=forms.Textarea, required=False) alt_address_complement = forms.CharField( label=_(u"Other address: address complement"), widget=forms.Textarea, required=False) alt_postal_code = forms.CharField(label=_(u"Other address: postal code"), max_length=10, required=False) alt_town = forms.CharField(label=_(u"Other address: town"), max_length=30, required=False) alt_country = forms.CharField(label=_(u"Other address: country"), max_length=30, required=False) def __init__(self, *args, **kwargs): super(SimplePersonForm, self).__init__(*args, **kwargs) self.fields['raw_name'].widget.attrs['readonly'] = True self.fields['title'].choices = models.TitleType.get_types( initial=self.init_data.get('title')) class PersonUserSelect(PersonSelect): ishtaruser__isnull = forms.NullBooleanField( label=_(u"Already has an account")) class PersonUserFormSelection(PersonFormSelection): SEARCH_AND_SELECT = True form_label = _(u"Person search") associated_models = {'pk': models.Person} currents = {'pk': models.Person} pk = forms.IntegerField( label="", widget=widgets.DataTable(reverse_lazy('get-person-for-account'), PersonUserSelect, models.Person, table_cols="TABLE_COLS_ACCOUNT"), validators=[models.valid_id(models.Person)]) class IshtarUserSelect(TableSelect): _model = models.IshtarUser search_vector = forms.CharField( label=_(u"Full text search"), widget=widgets.SearchWidget( 'ishtar-common', 'ishtaruser' )) username = forms.CharField(label=_(u"Username"), max_length=200) name = forms.CharField(label=_(u"Name"), max_length=200) surname = forms.CharField(label=_(u"Surname"), max_length=50) email = forms.CharField(label=_(u"Email"), max_length=75) person_types = forms.ChoiceField(label=_(u"Type"), choices=[]) attached_to = forms.IntegerField( label=_("Organization"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-organization'), associated_model=models.Organization), validators=[models.valid_id(models.Organization)]) def __init__(self, *args, **kwargs): super(IshtarUserSelect, self).__init__(*args, **kwargs) self.fields['person_types'].choices = models.PersonType.get_types() class AccountFormSelection(forms.Form): SEARCH_AND_SELECT = True form_label = _(u"Account search") associated_models = {'pk': models.IshtarUser} currents = {'pk': models.IshtarUser} pk = forms.IntegerField( label="", widget=widgets.DataTable(reverse_lazy('get-ishtaruser'), IshtarUserSelect, models.IshtarUser), validators=[models.valid_id(models.IshtarUser)]) class BasePersonForm(forms.ModelForm): class Meta: model = models.Person fields = ['title', 'salutation', 'name', 'surname', 'address', 'address_complement', 'town', 'postal_code'] class BaseOrganizationPersonForm(forms.ModelForm): class Meta: model = models.Person fields = ['attached_to', 'title', 'salutation', 'name', 'surname'] widgets = {'attached_to': widgets.JQueryPersonOrganization( reverse_lazy('autocomplete-organization'), reverse_lazy('organization_create'), model=models.Organization, attrs={'hidden': True}, new=True), } def __init__(self, *args, **kwargs): super(BaseOrganizationPersonForm, self).__init__(*args, **kwargs) def save(self, *args, **kwargs): person = super(BaseOrganizationPersonForm, self).save(*args, **kwargs) instance = person.attached_to form = BaseOrganizationForm(self.data, instance=instance, prefix=BaseOrganizationForm.form_prefix) if form.is_valid(): orga = form.save() if not person.attached_to: person.attached_to = orga person.save() return person class PersonForm(SimplePersonForm): person_types = forms.MultipleChoiceField( label=_("Person type"), choices=[], required=False, widget=forms.CheckboxSelectMultiple) def __init__(self, *args, **kwargs): super(PersonForm, self).__init__(*args, **kwargs) self.fields['person_types'].choices = models.PersonType.get_types( initial=self.init_data.get('person_types'), empty_first=False) self.fields['person_types'].help_text = models.PersonType.get_help() self.limit_fields() def save(self, user): dct = self.cleaned_data dct['history_modifier'] = user if 'attached_to' in dct and dct['attached_to']: try: dct['attached_to'] = models.Organization.objects.get( pk=dct['attached_to']) except models.Organization.DoesNotExist: dct.pop('attached_to') if 'title' in dct: try: dct['title'] = models.TitleType.objects.get( pk=dct['title']) except (models.TitleType.DoesNotExist, ValueError): dct.pop('title') person_types = dct.pop('person_types') new_item = models.Person.objects.create(**dct) for pt in person_types: new_item.person_types.add(pt) return new_item class NoOrgaPersonForm(PersonForm): def __init__(self, *args, **kwargs): super(NoOrgaPersonForm, self).__init__(*args, **kwargs) self.fields.pop('attached_to') class PersonTypeForm(ManageOldType, forms.Form): form_label = _("Person type") base_model = 'person_type' associated_models = {'person_type': models.PersonType} person_type = forms.MultipleChoiceField( label=_("Person type"), choices=[], required=False, widget=forms.CheckboxSelectMultiple) def __init__(self, *args, **kwargs): super(PersonTypeForm, self).__init__(*args, **kwargs) self.fields['person_type'].choices = models.PersonType.get_types( initial=self.init_data.get('person_type'), empty_first=False) self.fields['person_type'].help_text = models.PersonType.get_help() class AccountForm(IshtarForm): form_label = _("Account") associated_models = {'pk': models.Person} currents = {'pk': models.Person} pk = forms.IntegerField(label=u"", widget=forms.HiddenInput, required=False) username = forms.CharField(label=_(u"Account"), max_length=30) email = forms.CharField(label=_(u"Email"), max_length=75, validators=[validators.validate_email]) hidden_password = forms.CharField( label=_(u"New password"), max_length=128, widget=forms.PasswordInput, required=False, validators=[validators.MinLengthValidator(4)]) hidden_password_confirm = forms.CharField( label=_(u"New password (confirmation)"), max_length=128, widget=forms.PasswordInput, required=False) HEADERS = { 'hidden_password': FormHeader( _(u"New password"), help_message=_("Keep theses fields empty if you do not want to " "change password. On creation, if you leave these " "fields empty, the user will not be able to " "connect.")), } def __init__(self, *args, **kwargs): person = None if 'initial' in kwargs and 'pk' in kwargs['initial']: try: person = models.Person.objects.get(pk=kwargs['initial']['pk']) account = models.IshtarUser.objects.get(person=person).user_ptr if not kwargs['initial'].get('username'): kwargs['initial']['username'] = account.username if not kwargs['initial'].get('email'): kwargs['initial']['email'] = account.email except ObjectDoesNotExist: pass if 'person' in kwargs: person = kwargs.pop('person') super(AccountForm, self).__init__(*args, **kwargs) if person and person.raw_name: self.fields['username'].initial = \ person.raw_name.lower().replace(' ', '.') def clean(self): cleaned_data = self.cleaned_data password = cleaned_data.get("hidden_password") if password and \ password != cleaned_data.get("hidden_password_confirm"): raise forms.ValidationError(_(u"Your password and confirmation " u"password do not match.")) if not cleaned_data.get("pk"): models.is_unique(User, 'username')(cleaned_data.get("username")) if not password: raise forms.ValidationError(_(u"You must provide a correct " u"password.")) # check username unicity q = models.IshtarUser.objects.filter( user_ptr__username=cleaned_data.get('username')) if cleaned_data.get('pk'): q = q.exclude(person__pk=cleaned_data.get('pk')) if q.count(): raise forms.ValidationError(_(u"This username already exists.")) return cleaned_data class ProfileForm(ManageOldType): form_label = _("Profiles") base_model = 'profile' associated_models = { 'profile_type': models.ProfileType, 'area': models.Area } profile_type = forms.ChoiceField(label=_(u"Type"), choices=[]) area = widgets.Select2MultipleField(label=_(u"Areas"), required=False) name = forms.CharField(label=_(u"Name"), required=False) pk = forms.IntegerField(label=" ", widget=forms.HiddenInput, required=False) TYPES = [ FieldType('profile_type', models.ProfileType), FieldType('area', models.Area, is_multiple=True), ] ProfileFormset = formset_factory(ProfileForm, can_delete=True, formset=FormSetWithDeleteSwitches) ProfileFormset.form_label = _("Profiles") ProfileFormset.form_admin_name = _(u"Profiles") ProfileFormset.form_slug = "profiles" class FinalAccountForm(forms.Form): final = True form_label = _("Confirm") send_password = forms.BooleanField(label=_(u"Send the new password by " u"email?"), required=False) def __init__(self, *args, **kwargs): self.is_hidden = True return super(FinalAccountForm, self).__init__(*args, **kwargs) class ProfilePersonForm(forms.Form): """ Edit the current profile """ current_profile = forms.ChoiceField(label=_(u"Current profile"), choices=[]) name = forms.CharField(label=_(u"Name"), required=False) profile_type = forms.ChoiceField(label=_(u"Profile type"), required=False, disabled=True, choices=[]) auto_pin = forms.BooleanField( label=_(u"Pin automatically items on creation and modification"), required=False) display_pin_menu = forms.BooleanField( label=_(u"Show pin menu"), required=False) duplicate_profile = forms.BooleanField( label=_(u"Duplicate this profile"), required=False) delete_profile = forms.BooleanField( label=_(u"Delete this profile"), required=False, ) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') choices, initial = [], kwargs.get('initial', {}) current_profile = None for profile in self.user.ishtaruser.person.profiles.order_by( 'name', 'profile_type__label').all(): if profile.current: current_profile = profile initial['current_profile'] = profile.pk choices.append((profile.pk, str(profile))) if current_profile: initial['name'] = current_profile.name or \ current_profile.profile_type initial['profile_type'] = current_profile.profile_type.pk initial['auto_pin'] = current_profile.auto_pin initial['display_pin_menu'] = current_profile.display_pin_menu kwargs['initial'] = initial super(ProfilePersonForm, self).__init__(*args, **kwargs) self.fields['current_profile'].choices = choices if not current_profile or \ not self.user.ishtaruser.person.profiles.filter( profile_type=current_profile.profile_type).exclude( pk=current_profile.pk).count(): # cannot delete the current profile if no profile of this type is # available self.fields.pop('delete_profile') if not current_profile: return self.fields['profile_type'].choices = [ (current_profile.profile_type.pk, current_profile.profile_type.name) ] def clean(self): data = self.cleaned_data q = models.UserProfile.objects.filter( person__ishtaruser=self.user.ishtaruser, pk=data['current_profile']) if not q.count(): return data profile = q.all()[0] name = data.get('name', '') if models.UserProfile.objects.filter( person__ishtaruser=self.user.ishtaruser, name=name).exclude(pk=profile.pk).count(): raise forms.ValidationError( _(u"A profile with the same name exists.")) return data def save(self, session): q = models.UserProfile.objects.filter( person__ishtaruser=self.user.ishtaruser, pk=self.cleaned_data['current_profile']) if not q.count(): return profile = q.all()[0] # manage deletion if self.cleaned_data.get('delete_profile', None): q = self.user.ishtaruser.person.profiles.filter( profile_type=profile.profile_type).exclude( pk=profile.pk) if not q.count(): # cannot delete the current profile if no profile of this type # is available return new_current = q.all()[0] new_current.current = True new_current.save() profile.delete() return name = self.cleaned_data['name'] # manage duplication if self.cleaned_data.get('duplicate_profile', None): profile_name = profile.name or profile.profile_type.label if name == profile_name: name += str(_(u" (duplicate)")) profile.duplicate(name=name) return profile.current = True profile.name = name profile.auto_pin = self.cleaned_data['auto_pin'] profile.display_pin_menu = self.cleaned_data['display_pin_menu'] profile.save() clean_session_cache(session) class TownForm(forms.Form): form_label = _("Towns") base_model = 'town' associated_models = {'town': models.Town} town = get_town_field(required=False) class TownFormSet(FormSet): def clean(self): """Checks that no towns are duplicated.""" 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): from_key = '' to_key = '' def __init__(self, *args, **kwargs): self._cached_list = [] super(MergeFormSet, self).__init__(*args, **kwargs) def merge(self): for form in self.initial_forms: form.merge() def initial_form_count(self): """ Recopied from django source only get_queryset is changed """ if not (self.data or self.files): return len(self.get_restricted_queryset()) return super(MergeFormSet, self).initial_form_count() def _construct_form(self, i, **kwargs): """ Recopied from django source only get_queryset is changed """ if self.is_bound and i < self.initial_form_count(): # Import goes here instead of module-level because importing # django.db has side effects. # from django.db import connections pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name) pk = self.data[pk_key] """pk_field = self.model._meta.pk pk = pk_field.get_db_prep_lookup('exact', pk, connection=connections[self.get_queryset().db])""" pk = self.get_restricted_queryset()[i].pk if isinstance(pk, list): pk = pk[0] kwargs['instance'] = self._existing_object(pk) if i < self.initial_form_count() and not kwargs.get('instance'): kwargs['instance'] = self.get_restricted_queryset()[i] if i >= self.initial_form_count() and self.initial_extra: # Set initial values for extra forms try: kwargs['initial'] = \ self.initial_extra[i - self.initial_form_count()] except IndexError: pass return super(BaseModelFormSet, self)._construct_form(i, **kwargs) def get_restricted_queryset(self): """ Filter (from, to) when (to, from) is already here """ q = self.queryset if self._cached_list: return self._cached_list existing, res = [], [] # only get one version of each couple for item in q.all(): tpl = [getattr(item, self.from_key).pk, getattr(item, self.to_key).pk] if tpl not in existing: res.append(item) existing.append(list(reversed(tpl))) self._cached_list = res return res class MergeForm(forms.ModelForm): id = forms.IntegerField( label=u"", widget=forms.HiddenInput, required=False) a_is_duplicate_b = forms.BooleanField(required=False) b_is_duplicate_a = forms.BooleanField(required=False) not_duplicate = forms.BooleanField(required=False) def clean(self): checked = [True for k in ['a_is_duplicate_b', 'b_is_duplicate_a', 'not_duplicate'] if self.cleaned_data.get(k)] if len(checked) > 1: raise forms.ValidationError(_(u"Only one choice can be checked.")) return self.cleaned_data def merge(self, *args, **kwargs): try: to_item = getattr(self.instance, self.TO_KEY) from_item = getattr(self.instance, self.FROM_KEY) except ObjectDoesNotExist: return if self.cleaned_data.get('a_is_duplicate_b'): to_item.merge(from_item) elif self.cleaned_data.get('b_is_duplicate_a'): from_item.merge(to_item) elif self.cleaned_data.get('not_duplicate'): from_item.merge_exclusion.add(to_item) else: return try: self.instance.__class__.objects.get( **{self.TO_KEY: from_item, self.FROM_KEY: to_item}).delete() except ObjectDoesNotExist: pass self.instance.delete() class MergePersonForm(MergeForm): class Meta: model = models.Person fields = [] FROM_KEY = 'from_person' TO_KEY = 'to_person' class MergeOrganizationForm(MergeForm): class Meta: model = models.Organization fields = [] FROM_KEY = 'from_organization' TO_KEY = 'to_organization' def get_image_help(): if not settings.IMAGE_MAX_SIZE: return max_size_help() return str( _(u"Heavy images are resized to: %(width)dx%(height)d " u"(ratio is preserved).") \ % {'width': settings.IMAGE_MAX_SIZE[0], 'height': settings.IMAGE_MAX_SIZE[1]}) + " " + str( max_size_help()) ####################### # Document management # ####################### class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): form_label = _(u"Documentation") form_admin_name = _("Document - General") form_slug = "document-general" file_upload = True associated_models = {'source_type': models.SourceType} pk = forms.IntegerField(label="", required=False, widget=forms.HiddenInput) title = forms.CharField(label=_(u"Title"), required=False, validators=[validators.MaxLengthValidator(200)]) source_type = widgets.ModelChoiceField( model=models.SourceType, label=_(u"Source type"), choices=[], required=False) authors = widgets.ModelJQueryAutocompleteField( model=models.Author, multiple=True, label=_(u"Authors"), new=True, long_widget=True, required=False) associated_url = forms.URLField( max_length=1000, required=False, label=_(u"Numerical ressource (web address)")) image = forms.ImageField( label=_(u"Image"), help_text=mark_safe(get_image_help()), max_length=255, required=False, widget=widgets.ImageFileInput(), validators=[file_size_validator] ) associated_file = forms.FileField( label=pgettext(u"Not directory", u"File"), max_length=255, required=False, help_text=max_size_help(), validators=[file_size_validator] ) reference = forms.CharField( label=_(u"Reference"), validators=[validators.MaxLengthValidator(100)], required=False) internal_reference = forms.CharField( label=_(u"Internal reference"), validators=[validators.MaxLengthValidator(100)], required=False) receipt_date = forms.DateField(label=_(u"Receipt date"), required=False, widget=DatePicker) creation_date = forms.DateField(label=_(u"Creation date"), required=False, widget=DatePicker) receipt_date_in_documentation = forms.DateField( label=_(u"Receipt date in documentation"), required=False, widget=DatePicker) comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, required=False) description = forms.CharField(label=_(u"Description"), widget=forms.Textarea, required=False) additional_information = forms.CharField( label=_(u"Additional information"), widget=forms.Textarea, required=False) duplicate = forms.NullBooleanField(label=_(u"Has a duplicate"), required=False) TYPES = [ FieldType('source_type', models.SourceType), ] class Meta: model = models.Document fields = [ 'title', 'source_type', 'reference', 'internal_reference', 'image', 'associated_file', 'associated_url', 'authors', 'receipt_date', 'receipt_date_in_documentation', 'creation_date', 'comment', 'description', 'additional_information', 'duplicate' ] HEADERS = { 'title': FormHeader(_(u"Identification")), 'image': FormHeader(_(u"Content")), 'authors': FormHeader(_(u"Authors")), 'receipt_date': FormHeader(_(u"Dates")), 'comment': FormHeader(_(u"Advanced"), collapse=True), 'finds': FormHeader(_(u"Related items")), } def __init__(self, *args, **kwargs): main_items_fields = {} if "main_items_fields" in kwargs: main_items_fields = kwargs.pop("main_items_fields") super(DocumentForm, self).__init__(*args, **kwargs) for related_key in models.Document.RELATED_MODELS_ALT: model = models.Document._meta.get_field(related_key).related_model self.fields[related_key] = widgets.Select2MultipleField( model=model, remote=True, label=model._meta.verbose_name_plural, required=False, long_widget=True ) if related_key in main_items_fields: for field_key, label in main_items_fields[related_key]: disabled = False if kwargs.get('initial', None) and kwargs['initial'].get( field_key, False): disabled = True self.fields[field_key] = forms.BooleanField( label=label, required=False, disabled=disabled) def clean(self): cleaned_data = self.cleaned_data if not cleaned_data.get('title', None) and \ not cleaned_data.get('image', None) and \ not cleaned_data.get('associated_file', None) and \ not cleaned_data.get('associated_url', None): raise forms.ValidationError(_(u"You should at least fill one of " u"this field: title, url, image or " u"file.")) for rel in models.Document.RELATED_MODELS: if cleaned_data.get(rel, None): return cleaned_data raise forms.ValidationError(_(u"A document has to be attached at least " u"to one item")) def save(self, commit=True): if not self.cleaned_data.get('authors', None): self.cleaned_data['authors'] = [] item = super(DocumentForm, self).save(commit=commit) for related_key in models.Document.RELATED_MODELS: related = getattr(item, related_key) initial = dict([(rel.pk, rel) for rel in related.all()]) new = [int(pk) for pk in sorted(self.cleaned_data.get(related_key, []))] for pk in initial.keys(): if pk in new: continue related.remove(initial[pk]) for new_pk in new: related_item = related.model.objects.get(pk=new_pk) if new_pk not in initial.keys(): related.add(related_item) key = "{}_{}_main_image".format(related_key, related_item.pk) if self.cleaned_data.get(key, []) and \ related_item.main_image != item: related_item.skip_history_when_saving = True related_item.main_image = item related_item.save() item = models.Document.objects.get(pk=item.pk) item.skip_history_when_saving = True item.save() # resave to regen the attached items return item class DocumentSelect(HistorySelect): _model = models.Document form_admin_name = _(u"Document - 001 - Search") form_slug = "document-001-search" search_vector = forms.CharField( label=_(u"Full text search"), widget=widgets.SearchWidget( 'ishtar-common', 'document' )) authors = forms.IntegerField( widget=widgets.JQueryAutoComplete( "/" + settings.URL_PATH + 'autocomplete-author', associated_model=models.Author), validators=[models.valid_id(models.Author)], label=_(u"Author"), required=False) title = forms.CharField(label=_(u"Title")) source_type = forms.ChoiceField(label=_("Source type"), choices=[]) reference = forms.CharField(label=_(u"Reference")) internal_reference = forms.CharField(label=_(u"Internal reference")) description = forms.CharField(label=_(u"Description")) comment = forms.CharField(label=_(u"Comment")) additional_information = forms.CharField( label=_(u"Additional informations")) duplicate = forms.NullBooleanField(label=_(u"Has a duplicate")) image__isnull = forms.NullBooleanField(label=_(u"Has an image?")) operation = forms.IntegerField( label=_(u"Operation"), required=False, widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-operation'), associated_model=Operation), validators=[models.valid_id(Operation)]) context_record = forms.IntegerField( label=_(u"Context record"), required=False, widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-contextrecord'), associated_model=ContextRecord), validators=[models.valid_id(ContextRecord)]) find = forms.IntegerField( label=_(u"Find"), required=False, widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-find'), associated_model=Find), validators=[models.valid_id(Find)]) find__denomination = forms.CharField(label=_(u"Find - denomination"), required=False) container = forms.IntegerField( label=_(u"Container"), required=False, widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-container'), associated_model=Container), validators=[models.valid_id(Container)]) TYPES = [ FieldType('source_type', models.SourceType), ] PROFILE_FILTER = { 'context_record': ['context_record'], 'find': ['find'], 'warehouse': ['container'] } class DocumentFormSelection(CustomFormSearch): SEARCH_AND_SELECT = True form_label = _(u"Document search") associated_models = {'pk': models.Document} currents = {'pk': models.Document} pk = forms.IntegerField( label="", required=False, widget=widgets.DataTable( reverse_lazy('get-document'), DocumentSelect, models.Document, gallery=True ), validators=[models.valid_id(models.Document)]) def clean(self): cleaned_data = self.cleaned_data if 'pk' not in cleaned_data or not cleaned_data['pk']: raise forms.ValidationError(_(u"You should select an item.")) return cleaned_data class QADocumentFormMulti(QAForm): form_admin_name = _(u"Document - Quick action - Modify") form_slug = "document-quickaction-modify" base_models = ['qa_source_type'] associated_models = { 'qa_source_type': models.SourceType, } MULTI = True REPLACE_FIELDS = [ 'qa_source_type', ] qa_source_type = forms.ChoiceField( label=_(u"Source type"), required=False ) TYPES = [ FieldType('qa_source_type', models.SourceType), ] class QALockForm(forms.Form): action = forms.ChoiceField( label=_("Action"), choices=(('lock', _("Lock")), ('unlock', _("Unlock")))) def __init__(self, *args, **kwargs): self.items = kwargs.pop('items') super(QALockForm, self).__init__(*args, **kwargs) def save(self, items, user): locked = self.cleaned_data["action"] == "lock" for item in items: item.locked = locked if locked: item.lock_user = user else: item.lock_user = None item.skip_history_when_saving = True item.save() class SourceDeletionForm(FinalForm): confirm_msg = " " confirm_end_msg = _(u"Would you like to delete this documentation?") ###################### # Authors management # ###################### class AuthorForm(ManageOldType, NewItemForm): form_label = _(u"Author") associated_models = {'person': models.Person, 'author_type': models.AuthorType} person = forms.IntegerField( widget=widgets.JQueryAutoComplete( "/" + settings.URL_PATH + 'autocomplete-person', associated_model=models.Person, new=True), validators=[models.valid_id(models.Person)], label=_(u"Person")) author_type = forms.ChoiceField(label=_(u"Author type"), choices=[]) def __init__(self, *args, **kwargs): super(AuthorForm, self).__init__(*args, **kwargs) self.fields['author_type'].choices = models.AuthorType.get_types( initial=self.init_data.get('author_type')) self.limit_fields() def clean(self): person_id = self.cleaned_data.get("person", None) author_type_id = self.cleaned_data.get("author_type", None) if not person_id or not author_type_id: return self.cleaned_data if models.Author.objects.filter(author_type_id=author_type_id, person_id=person_id).count(): raise forms.ValidationError(_("This author already exist.")) def save(self, user): dct = self.cleaned_data dct['author_type'] = models.AuthorType.objects.get( pk=dct['author_type']) dct['person'] = models.Person.objects.get(pk=dct['person']) new_item = models.Author(**dct) new_item.save() return new_item class AuthorFormSelection(forms.Form): form_label = _(u"Author selection") base_model = 'author' associated_models = {'author': models.Author} author = forms.IntegerField( required=False, widget=widgets.JQueryAutoComplete( "/" + settings.URL_PATH + 'autocomplete-author', associated_model=models.Author, new=True), validators=[models.valid_id(models.Author)], label=_(u"Author")) class AuthorFormSet(FormSet): def clean(self): """Checks that no author are duplicated.""" return self.check_duplicate(('author',), _("There are identical authors.")) AuthorFormset = formset_factory(AuthorFormSelection, can_delete=True, formset=AuthorFormSet) AuthorFormset.form_label = _("Authors") AuthorFormset.form_admin_name = _(u"Authors") AuthorFormset.form_slug = "authors" class SearchQueryForm(forms.Form): query = forms.CharField(max_length=None, label=_(u"Query"), initial='*', widget=forms.HiddenInput) search_query = forms.ChoiceField(label="", required=False, choices=[]) label = forms.CharField(label="", max_length=None, required=False) is_alert = forms.BooleanField(label=_(u"Is an alert"), required=False) create_or_update = forms.ChoiceField( choices=(('create', _(u"Create")), ('update', _(u"Update"))), initial='create') def __init__(self, profile, content_type, *args, **kwargs): self.profile = profile self.content_type = content_type super(SearchQueryForm, self).__init__(*args, **kwargs) self.fields['search_query'].choices = [ (c.pk, c.label) for c in models.SearchQuery.objects.filter( content_type=content_type, profile=profile).all()] if not self.fields['search_query'].choices: self.fields.pop('search_query') def clean(self): data = self.cleaned_data if data['create_or_update'] == 'create' and not data['label']: raise forms.ValidationError(_(u"A label is required for a new " u"search query.")) elif data['create_or_update'] == 'update': if not data['search_query']: raise forms.ValidationError(_(u"Select the search query to " u"update")) q = models.SearchQuery.objects.filter( profile=self.profile, content_type=self.content_type, pk=data['search_query']) if not q.count(): raise forms.ValidationError(_(u"Query does not exist.")) return data def save(self): data = self.cleaned_data if data['create_or_update'] == 'create': sq = models.SearchQuery.objects.create( label=data['label'], query=data['query'], profile=self.profile, content_type=self.content_type, is_alert=data['is_alert']) else: try: sq = models.SearchQuery.objects.get( profile=self.profile, content_type=self.content_type, pk=data['search_query']) except models.SearchQuery.DoesNotExist: raise forms.ValidationError(_(u"Query does not exist.")) sq.query = data['query'] sq.save() return sq