Exemple #1
0
class RTypeBrickItemAddCtypeForm(CremeModelForm):
    ctype = EntityCTypeChoiceField(
        label=_('Customised resource'),
        widget=DynamicSelect({'autocomplete': True}),
    )

    class Meta:
        model = RelationBrickItem
        exclude = ('relation_type', )

    def __init__(self, *args, **kwargs):
        # super(RTypeBrickItemAddCtypeForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        instance = self.instance
        ct_field = self.fields['ctype']
        compatible_ctypes = instance.relation_type.object_ctypes.all()

        if compatible_ctypes:
            ct_field.ctypes = compatible_ctypes

        used_ct_ids = frozenset(
            ct.id
            for ct, cells in instance.iter_cells())  # TODO: iter_ctypes() ??
        ct_field.ctypes = (ct for ct in ct_field.ctypes
                           if ct.id not in used_ct_ids)

    def save(self, *args, **kwargs):
        self.instance.set_cells(self.cleaned_data['ctype'], ())

        # return super(RTypeBrickItemAddCtypeForm, self).save(*args, **kwargs)
        return super().save(*args, **kwargs)
Exemple #2
0
    def get_context(self, name, value, attrs):
        context = super().get_context(name=name, value=value, attrs=attrs)
        url = self.creation_url

        if url:
            final_attrs = context['widget']['attrs']

            if final_attrs is None or not ('disabled' in final_attrs
                                           or 'readonly' in final_attrs):
                button_list = ActionButtonList(
                    delegate=DynamicSelect(options=self.choices),
                    attrs=self.attrs,
                )

                allowed = self.creation_allowed
                label = str(self.creation_label)
                button_list.add_action(
                    'create',
                    label,
                    icon='add',
                    title=label if allowed else gettext('Cannot create'),
                    enabled=allowed,
                    popupUrl=url,
                )

                context = button_list.get_context(name=name,
                                                  value=value,
                                                  attrs=attrs)

        return context
Exemple #3
0
class CustomBrickConfigItemCreateForm(CremeModelForm):
    ctype = EntityCTypeChoiceField(
        label=_('Related resource'),
        widget=DynamicSelect(attrs={'autocomplete': True}),
    )

    class Meta(CremeModelForm.Meta):
        model = CustomBrickConfigItem

    def __init__(self, *args, **kwargs):
        # super(CustomBrickConfigItemCreateForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        # TODO: add an 'exclude' argument in creme_entity_content_types() ??
        get_for_model = ContentType.objects.get_for_model
        is_invalid = gui_bricks.brick_registry.is_model_invalid
        self.fields['ctype'].ctypes = (
            get_for_model(model)
            for model in creme_registry.iter_entity_models()
            if not is_invalid(model))

    def save(self, *args, **kwargs):
        instance = self.instance
        ct = self.cleaned_data['ctype']
        instance.content_type = ct

        # super(CustomBrickConfigItemCreateForm, self).save(commit=False)
        super().save(commit=False)
        generate_string_id_and_save(
            CustomBrickConfigItem, [instance],
            'creme_core-user_customblock_{}-{}'.format(ct.app_label, ct.model))

        return instance
Exemple #4
0
        def _add_related_orga_fields(form):
            from django.contrib.contenttypes.models import ContentType
            from django.forms import ModelChoiceField

            from creme.creme_core.forms.widgets import DynamicSelect
            from creme.creme_core.models import RelationType

            fields = form.fields
            get_ct = ContentType.objects.get_for_model
            fields['organisation'] = ModelChoiceField(
                label=_('User organisation'),
                queryset=self.Organisation.objects.filter_managed_by_creme(),
                empty_label=None,
            )
            fields['relation'] = ModelChoiceField(
                label=_('Position in the organisation'),
                queryset=RelationType.objects.filter(
                    subject_ctypes=get_ct(self.Contact),
                    object_ctypes=get_ct(self.Organisation),
                    is_internal=False,
                ),
                empty_label=None,
                widget=DynamicSelect(attrs={'autocomplete': True}),
                initial=constants.REL_SUB_EMPLOYED_BY,
            )

            def set_required(name):
                field = fields[name]
                field.required = field.widget.is_required = True

            set_required('first_name')
            set_required('last_name')
            set_required('email')
Exemple #5
0
class CustomBrickConfigItemCreateForm(_CustomBrickConfigItemBaseForm):
    ctype = EntityCTypeChoiceField(
        label=_('Related resource'),
        widget=DynamicSelect(attrs={'autocomplete': True}),
    )

    # class Meta(CremeModelForm.Meta):
    #     model = CustomBrickConfigItem

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # TODO: add an 'exclude' argument in creme_entity_content_types() ??
        get_for_model = ContentType.objects.get_for_model
        is_invalid = gui_bricks.brick_registry.is_model_invalid
        self.fields['ctype'].ctypes = (
            get_for_model(model)
            for model in creme_registry.iter_entity_models()
            if not is_invalid(model))

    def clean(self, *args, **kwargs):
        cdata = super().clean(*args, **kwargs)

        if not self._errors:
            self.instance.content_type = self.cleaned_data['ctype']

        return cdata
Exemple #6
0
class RTypeBrickAddForm(CremeModelForm):
    relation_type = ModelChoiceField(
        RelationType.objects,
        empty_label=None,
        widget=DynamicSelect(attrs={'autocomplete': True}),
    )

    class Meta(CremeModelForm.Meta):
        model = RelationBrickItem

    def __init__(self, *args, **kwargs):
        # super(RTypeBrickAddForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)

        existing_type_ids = RelationBrickItem.objects.values_list(
            'relation_type_id', flat=True)

        relation_type = self.fields['relation_type']
        relation_type.queryset = RelationType.objects.exclude(
            pk__in=existing_type_ids)

    def save(self, *args, **kwargs):
        self.instance.brick_id = gui_bricks.SpecificRelationsBrick.generate_id(
            'creme_config',
            self.cleaned_data['relation_type'].id,
        )
        # return super(RTypeBrickAddForm, self).save(*args, **kwargs)
        return super().save(*args, **kwargs)
Exemple #7
0
class FieldsConfigAddForm(CremeForm):
    ctype = CTypeChoiceField(label=_(u'Related resource'),
                             help_text=_(u'The proposed types of resource have '
                                         u'at least a field which can be hidden.'
                                        ),
                             widget=DynamicSelect(attrs={'autocomplete': True}),
                            )

    def __init__(self, *args, **kwargs):
        # super(FieldsConfigAddForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        used_ct_ids = set(FieldsConfig.objects.values_list('content_type', flat=True))
        self.ctypes = ctypes = [ct for ct in map(ContentType.objects.get_for_model,
                                                 filter(FieldsConfig.is_model_valid, apps.get_models())
                                                )
                                        if ct.id not in used_ct_ids
                               ]

        if ctypes:
            self.fields['ctype'].ctypes = ctypes
        else:
            # TODO: remove the 'submit' button ?
            self.fields['ctype'] = CharField(
                    label=_(u'Related resource'),
                    required=False, widget=Label,
                    initial=_(u'All configurable types of resource are already configured.'),
                )

    def save(self):
        if self.ctypes:
            return FieldsConfig.objects.create(content_type=self.cleaned_data['ctype'],
                                               descriptions=(),
                                              )
class EditCredentialsForm(CremeModelForm):
    can_view = BooleanField(label=_('Can view'), required=False)
    can_change = BooleanField(label=_('Can change'), required=False)
    can_delete = BooleanField(label=_('Can delete'), required=False)
    can_link = BooleanField(
        label=_('Can link'),
        required=False,
        help_text=_('You must have the permission to link on 2 entities'
                    ' to create a relationship between them.'),
    )
    can_unlink = BooleanField(
        label=_('Can unlink'),
        required=False,
        help_text=_('You must have the permission to unlink on 2 entities'
                    ' to delete a relationship between them.'),
    )
    set_type = ChoiceField(
        label=_('Type of entities set'),
        choices=SetCredentials.ESETS_MAP.items(),
        widget=CremeRadioSelect,
    )
    ctype = EntityCTypeChoiceField(
        label=_('Apply to a specific type'),
        widget=DynamicSelect(attrs={'autocomplete': True}),
        required=False,
        empty_label=pgettext('content_type', 'None'),
    )

    class Meta:
        model = SetCredentials
        exclude = ('role', 'value')  # fields ??

    def __init__(self, *args, **kwargs):
        # super(EditCredentialsForm, self).__init__(*args, **kwargs)
        super().__init__(*args, **kwargs)
        fields = self.fields
        fields['ctype'].ctypes = filtered_entity_ctypes(
            self._get_allowed_apps())

        # TODO: SetCredentials.value default to 0
        value = self.instance.value or 0
        fields['can_view'].initial = bool(value & EntityCredentials.VIEW)
        fields['can_change'].initial = bool(value & EntityCredentials.CHANGE)
        fields['can_delete'].initial = bool(value & EntityCredentials.DELETE)
        fields['can_link'].initial = bool(value & EntityCredentials.LINK)
        fields['can_unlink'].initial = bool(value & EntityCredentials.UNLINK)

    def _get_allowed_apps(self):
        return self.instance.role.allowed_apps

    def save(self, *args, **kwargs):
        get_data = self.cleaned_data.get

        self.instance.set_value(get_data('can_view'), get_data('can_change'),
                                get_data('can_delete'), get_data('can_link'),
                                get_data('can_unlink'))

        # return super(EditCredentialsForm, self).save(*args, **kwargs)
        return super().save(*args, **kwargs)
Exemple #9
0
    def test_options_generator(self):
        select = DynamicSelect(options=((id_, str(id_)) for id_ in range(10)))

        self.assertIsList(select.options)
        self.assertListEqual(
            [(id_, str(id_)) for id_ in range(10)],
            select.choices,
        )
Exemple #10
0
    def test_options_function(self):
        select = DynamicSelect(
            options=lambda: [(id, str(id)) for id in range(10)])

        self.assertTrue(callable(select.options))
        self.assertListEqual(
            [(id_, str(id_)) for id_ in range(10)],
            select.choices,
        )
    def test_options_queryset(self):
        user = self.login()
        FakeContact.objects.create(last_name='Doe', first_name='John', user=user)

        select = DynamicSelect(options=FakeContact.objects.values_list('id', 'last_name'))
        self.assertIsInstance(select.options, QuerySet)
        self.assertListEqual(list(FakeContact.objects.values_list('id', 'last_name')),
                             list(select.choices)
                            )
Exemple #12
0
        class ContactUserCreationForm(UserCreation.form_class):
            organisation = ModelChoiceField(
                label=_('User organisation'),
                queryset=self.Organisation.objects.filter_managed_by_creme(),
                empty_label=None,
            )
            relation = ModelChoiceField(
                label=_('Position in the organisation'),
                # NB: the QuerySet is built in __init__() because a loading artefact
                #     makes ContentType values inconsistent in unit tests if the
                #     Queryset is built here.
                queryset=RelationType.objects.none(),
                empty_label=None,
                widget=DynamicSelect(attrs={'autocomplete': True}),
                initial=constants.REL_SUB_EMPLOYED_BY,
            )

            blocks = UserCreation.form_class.blocks.new({
                'id':
                'contact',
                'label':
                _('Related Contact'),
                'fields': ('organisation', 'relation'),
            })

            def __init__(this, *args, **kwargs):
                super().__init__(*args, **kwargs)
                fields = this.fields

                get_ct = ContentType.objects.get_for_model
                fields['relation'].queryset = RelationType.objects.filter(
                    subject_ctypes=get_ct(self.Contact),
                    symmetric_type__subject_ctypes=get_ct(self.Organisation),
                    is_internal=False,
                )

                for field_name in ('first_name', 'last_name', 'email'):
                    field = fields[field_name]
                    field.required = field.widget.is_required = True

            def save(this, *args, **kwargs):
                user = super().save(*args, **kwargs)
                cdata = this.cleaned_data

                Relation.objects.create(
                    user=user,
                    subject_entity=user.linked_contact,
                    type=cdata['relation'],
                    object_entity=cdata['organisation'],
                )

                return user
class CustomFieldsCTAddForm(CustomFieldsBaseForm):
    content_type = EntityCTypeChoiceField(
        label=_('Related resource'),
        help_text=_('The other custom fields for this type of resource '
                    'will be chosen by editing the configuration'),
        widget=DynamicSelect({'autocomplete': True}))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        used_ct_ids = {
            *CustomField.objects.values_list('content_type_id', flat=True)
        }
        ct_field = self.fields['content_type']
        ct_field.ctypes = (ct for ct in ct_field.ctypes
                           if ct.id not in used_ct_ids)
Exemple #14
0
    def test_render(self):
        select = DynamicSelect(options=[(1, 'A'), (2, 'B')])
        self.assertHTMLEqual(
            '<select class="ui-creme-input ui-creme-widget widget-auto ui-creme-dselect" '
            ' name="test" url="" widget="ui-creme-dselect">'
            '  <option value="1">A</option>'
            '  <option value="2" selected>B</option>'
            '</select>', select.render('test', 2))

        Choice = DynamicSelect.Choice
        select = DynamicSelect(options=[
            (Choice(1, disabled=True, help='disabled'), 'A'),
            (Choice(2, help='item B'), 'B'),
            (Choice(3, help='item C'), 'C'),
            (Choice(4, readonly=True, help='readonly'), 'D'),
        ], )
        self.assertHTMLEqual(
            '<select class="ui-creme-input ui-creme-widget widget-auto ui-creme-dselect" '
            ' name="test" url="" widget="ui-creme-dselect">'
            '  <option value="1" disabled help="disabled">A</option>'
            '  <option value="2" selected help="item B">B</option>'
            '  <option value="3" help="item C">C</option>'
            '  <option value="4" readonly help="readonly">D</option>'
            '</select>', select.render('test', 2))
Exemple #15
0
class GraphFetcherField(Field):
    widget = DynamicSelect(attrs={'autocomplete': True})
    default_error_messages = {
        'invalid_choice': _(
            'Select a valid choice. %(value)s is not one of the available choices.'
        ),
    }

    _graph: 'AbstractReportGraph'
    choice_iterator_class = FetcherChoiceIterator
    _choice_separator: str  # Separate the type & the value of each fetcher choice

    def __init__(self, *, graph=None, choice_separator='|', **kwargs):
        super().__init__(**kwargs)
        self._choice_separator = choice_separator
        self.graph = graph

    def _update_choices(self):
        self.widget.choices = self.choice_iterator_class(
            graph=self._graph,
            separator=self._choice_separator,
        )

    @property
    def choice_separator(self):
        return self._choice_separator

    @choice_separator.setter
    def choice_separator(self, sep):
        self._choice_separator = sep
        self._update_choices()

    @property
    def graph(self):
        return self._graph

    @graph.setter
    def graph(self, graph):
        self._graph = graph
        self._update_choices()

    def to_python(self, value):
        """Returns a GraphFetcher."""
        if not value:
            return None

        fetcher_type_id, __, fetcher_value = value.partition(self._choice_separator)
        graph = self.graph
        fetcher = graph.fetcher_registry.get(
            graph=graph,
            fetcher_dict={
                GraphFetcher.DICT_KEY_TYPE:  fetcher_type_id,
                GraphFetcher.DICT_KEY_VALUE: fetcher_value,
            },
        )

        if fetcher.error:
            raise ValidationError(
                self.error_messages['invalid_choice'],
                code='invalid_choice',
                params={'value': value},
            )

        return fetcher
Exemple #16
0
    def test_options_list(self):
        select = DynamicSelect(options=[(1, 'A'), (2, 'B')])

        self.assertIsList(select.options)
        self.assertListEqual([(1, 'A'), (2, 'B')], select.choices)
Exemple #17
0
class GraphInstanceBrickForm(CremeForm):
    volatile_column = ChoiceField(
        label=_('Volatile column'),
        choices=(),
        required=False,
        widget=DynamicSelect(attrs={'autocomplete': True}),
        help_text=_(
            'When the graph is displayed on the detail-view of an entity, '
            'only the entities linked to this entity by the following link '
            'are used to compute the graph.'),
    )

    def __init__(self, graph, instance=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.graph = graph
        self.fields['volatile_column'].choices = self._get_volatile_choices(
            graph.linked_report.ct)

    def _get_volatile_choices(self, ct):
        choices = []
        fk_choices = [('fk-' + name, vname)
                      for name, vname in ModelFieldEnumerator(
                          ct.model_class(), deep=0, only_leafs=False).filter(
                              (lambda f, deep: isinstance(f, ForeignKey) and
                               issubclass(f.remote_field.model, CremeEntity)),
                              viewable=True,
                          ).choices()]

        self._rtypes = {}
        rtype_choices = []

        for rtype in RelationType.objects.compatible(ct,
                                                     include_internals=True):
            rtype_choices.append(('rtype-' + rtype.id, str(rtype)))
            self._rtypes[rtype.id] = rtype

        if fk_choices:
            choices.append((_('Fields'), fk_choices))

        if rtype_choices:
            choices.append((_('Relationships'), rtype_choices))

        if not choices:
            choices.append(('', _('No available choice')))
        else:
            choices.insert(
                0, ('', pgettext_lazy('reports-volatile_choice', 'None')))

        return choices

    def clean(self):
        cleaned_data = super().clean()
        volatile_column = cleaned_data.get('volatile_column')
        kwargs = {}

        if volatile_column:
            link_type, link_val = volatile_column.split('-', 1)

            if link_type == 'fk':
                kwargs['volatile_field'] = link_val
            else:
                kwargs['volatile_rtype'] = self._rtypes[link_val]

        try:
            self.ibci = self.graph.create_instance_brick_config_item(
                save=False, **kwargs)
        except InstanceBrickConfigItemError as e:
            raise ValidationError(str(e)) from e

        return cleaned_data

    def save(self):
        ibci = self.ibci
        ibci.save()

        return ibci
Exemple #18
0
class FieldsConfigAddForm(CremeModelForm):
    ctype = core_fields.CTypeChoiceField(
        label=_('Related resource'),
        help_text=
        _('The proposed types of resource have at least a field which can be hidden.'
          ),
        widget=DynamicSelect(attrs={'autocomplete': True}),
    )

    class Meta(CremeModelForm.Meta):
        model = FieldsConfig

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        models = [
            *filter(FieldsConfig.objects.is_model_valid, apps.get_models())
        ]
        # NB: we use <FieldsConfig.objects.get_for_models()> to take advantage of its cache ;
        #     it useful because this constructor can be called several times in a request
        #     because of our wizard (which fill the instance by calling all
        #     previous steps' validation).
        # Old code:
        #  used_ct_ids = {*FieldsConfig.objects.values_list('content_type', flat=True)}
        excluded_ct_ids = {
            # Do not want a choice "creme entity" ('description' can be hidden).
            ContentType.objects.get_for_model(CremeEntity).id,

            # Exclude ContentType which already have a configuration
            *(
                fc.content_type_id
                for fc in FieldsConfig.objects.get_for_models(models).values()
                if
                not fc._state.adding  # <True> means the FieldsConfig is in DB
            )
        }
        self.ctypes = ctypes = [
            ct for ct in map(ContentType.objects.get_for_model, models)
            if ct.id not in excluded_ct_ids
        ]

        if ctypes:
            self.fields['ctype'].ctypes = ctypes
        else:
            # TODO: remove the 'submit' button ?
            self.fields['ctype'] = core_fields.ReadonlyMessageField(
                label=_('Related resource'),
                initial=_(
                    'All configurable types of resource are already configured.'
                ),
            )

    def clean(self, *args, **kwargs):
        cdata = super().clean(*args, **kwargs)

        if not self._errors:
            instance = self.instance
            instance.content_type = self.cleaned_data['ctype']
            instance.descriptions = ()

        return cdata

    def save(self, *args, **kwargs):
        if self.ctypes:  # NB: remove if we raise a ValidationError in clean()
            super().save(*args, **kwargs)

        return self.instance
Exemple #19
0
class VcfImportForm(CremeModelForm):
    class Meta:
        model = Contact
        fields = ('user', 'civility', 'first_name', 'last_name', 'position')

    vcf_step = IntegerField(widget=HiddenInput)

    image_encoded = CharField(required=False, widget=HiddenInput)

    # Details
    phone = CharField(label=_('Phone number'), required=False)
    mobile = CharField(label=_('Mobile'), required=False)
    fax = CharField(label=_('Fax'), required=False)
    email = EmailField(label=_('Email address'), required=False)
    url_site = URLField(label=_('Web Site'), required=False)

    # Address
    homeaddr_name = CharField(label=_('Name'), required=False)
    homeaddr_address = CharField(label=_('Address'), required=False)
    homeaddr_city = CharField(label=_('City'), required=False)
    homeaddr_country = CharField(label=_('Country'), required=False)
    homeaddr_code = CharField(label=_('Zip code'), required=False)
    homeaddr_region = CharField(label=_('Region'), required=False)

    # Related Organisation
    create_or_attach_orga = BooleanField(
        label=_('Create or attach organisation'),
        required=False,
        initial=False,
    )
    organisation = CreatorEntityField(label=_('Organisation'),
                                      required=False,
                                      model=Organisation)
    relation = ModelChoiceField(
        label=_('Position in the organisation'),
        queryset=RelationType.objects.none(),
        initial=REL_SUB_EMPLOYED_BY,
        required=False,
        empty_label='',
        widget=DynamicSelect(attrs={'autocomplete': True}),
    )

    # TODO: Composite field
    update_work_name = BooleanField(
        label=_('Update name'),
        required=False,
        initial=False,
        help_text=_('Update organisation selected name'))
    update_work_phone = BooleanField(
        label=_('Update phone'),
        required=False,
        initial=False,
        help_text=_('Update organisation selected phone'))
    update_work_fax = BooleanField(
        label=_('Update fax'),
        required=False,
        initial=False,
        help_text=_('Update organisation selected fax'))
    update_work_email = BooleanField(
        label=_('Update email'),
        required=False,
        initial=False,
        help_text=_('Update organisation selected email'))
    update_work_url_site = BooleanField(
        label=_('Update web site'),
        required=False,
        initial=False,
        help_text=_('Update organisation selected web site'))
    update_work_address = BooleanField(
        label=_('Update address'),
        required=False,
        initial=False,
        help_text=_('Update organisation selected address'))

    # Organisation name & details
    work_name = CharField(label=_('Name'), required=False)
    work_phone = CharField(label=_('Phone number'), required=False)
    work_fax = CharField(label=_('Fax'), required=False)
    work_email = EmailField(label=_('Email address'), required=False)
    work_url_site = URLField(label=_('Web Site'), required=False)

    # Organisation address
    workaddr_name = CharField(label=_('Name'), required=False)
    workaddr_address = CharField(label=_('Address'), required=False)
    workaddr_city = CharField(label=_('City'), required=False)
    workaddr_country = CharField(label=_('Country'), required=False)
    workaddr_code = CharField(label=_('Zip code'), required=False)
    workaddr_region = CharField(label=_('Region'), required=False)

    error_messages = {
        'required4orga': _('Required, if you want to create organisation'),
        'no_orga_creation': _('Create organisation not checked'),
        'orga_not_selected': _('Organisation not selected'),
        'required2update': _('Required, if you want to update organisation'),
    }

    # Names of the fields corresponding to the Contact's details.
    contact_details = ['phone', 'mobile', 'fax', 'email', 'url_site']

    # Names of the fields corresponding to the related Organisation (but not its Address).
    orga_fields = ['name', 'phone', 'email', 'fax', 'url_site']

    # Correspondence between VCF field types & form-field names.
    phone_dict = {
        'HOME': 'phone',
        'CELL': 'mobile',
        'FAX': 'fax',
        'WORK': 'work_phone',
    }
    email_dict = {
        'HOME': 'email',
        'INTERNET': 'email',
        'WORK': 'work_email',
    }
    url_dict = {
        'HOME': 'url_site',
        'INTERNET': 'url_site',
        'WORK': 'work_url_site',
    }

    # Form-field names prefix for address + correspondence with VCF field types.
    address_prefixes = {
        'HOME': HOME_ADDR_PREFIX,
        'WORK': WORK_ADDR_PREFIX,
    }
    # Mapping between form fields names (which use vcf lib names) & Address fields names.
    address_mapping = [
        ('name', 'name'),
        ('address', 'address'),
        ('city', 'city'),
        ('country', 'country'),
        ('code', 'zipcode'),
        ('region', 'department'),
    ]

    # blocks = CremeModelWithUserForm.blocks.new(
    blocks = CremeModelForm.blocks.new(
        ('details', _('Details'), contact_details),
        ('contact_address', _('Billing address'),
         [HOME_ADDR_PREFIX + n[0] for n in address_mapping]),
        ('organisation', _('Organisation'), [
            'create_or_attach_orga', 'organisation', 'relation',
            *chain.from_iterable(
                ('update_work_' + fn, 'work_' + fn) for fn in orga_fields)
        ]),
        ('organisation_address', _('Organisation billing address'), [
            'update_work_address', *(WORK_ADDR_PREFIX + n[0]
                                     for n in address_mapping)
        ]),
    )

    type_help_text = _('Read in VCF File without type : ')
    other_help_text = _('Read in VCF File : ')

    def __init__(self, vcf_data=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        fields = self.fields

        if vcf_data:
            self._init_contact_fields(vcf_data)
            self._init_orga_field(vcf_data)
            self._init_addresses_fields(vcf_data)

            if vcf_data.contents.get('photo'):
                fields['image_encoded'].initial = vcf_data.photo.value.replace(
                    '\n', '')

        # Beware: this queryset directly in the field declaration does not work on some systems in unit tests...
        #         (it seems that the problem it caused by the M2M - other fields work, but why ???)
        fields['relation'].queryset = RelationType.objects.filter(
            subject_ctypes=_get_ct(Contact),
            object_ctypes=_get_ct(Organisation),
        )

        self._hide_fields()

    def _hide_fields(self):
        fields = self.fields
        address_mapping = self.address_mapping
        fconfigs = FieldsConfig.get_4_models((Contact, Organisation, Address))

        # TODO: use shipping address if not hidden ?
        if fconfigs[Contact].is_fieldname_hidden('billing_address'):
            prefix = HOME_ADDR_PREFIX

            for form_fname, __ in address_mapping:
                del fields[prefix + form_fname]

        is_orga_field_hidden = fconfigs[Organisation].is_fieldname_hidden
        for fname in [*fields
                      ]:  # NB: Cannot mutate the OrderedDict during iteration.
            # NB: 5 == len('work_')
            if fname.startswith('work_') and is_orga_field_hidden(fname[5:]):
                del fields[fname]
                del fields['update_' + fname]

        if is_orga_field_hidden('billing_address'):
            prefix = WORK_ADDR_PREFIX

            for form_fname, __ in address_mapping:
                del fields[prefix + form_fname]

            del fields['update_work_address']

        is_addr_field_hidden = fconfigs[Address].is_fieldname_hidden
        addr_prefixes = self.address_prefixes.values()
        for form_fname, model_fname in address_mapping:
            if is_addr_field_hidden(model_fname):
                for prefix in addr_prefixes:
                    fields.pop(prefix + form_fname, None)

    def _init_contact_fields(self, vcf_data):
        contents = vcf_data.contents
        fields = self.fields
        contact_data = contents.get('n')

        if contact_data:
            value = vcf_data.n.value
            last_name = value.family
            fields['first_name'].initial = value.given
            fields['last_name'].initial = last_name
            fields['homeaddr_name'].initial = last_name
            prefix = value.prefix

            if prefix:
                # TODO: find in title too ?
                civ = Civility.objects.filter(
                    shortcut__icontains=prefix).first()
                if civ:
                    fields['civility'].initial = civ.id
                else:
                    fields[
                        'civility'].help_text = self.other_help_text + prefix
        else:
            first_name, sep, last_name = vcf_data.fn.value.partition(' ')
            fields['first_name'].initial = first_name
            fields['last_name'].initial = last_name
            fields['homeaddr_name'].initial = last_name

        if contents.get('title'):
            title = vcf_data.title.value
            position = Position.objects.filter(title__icontains=title).first()

            if position:
                fields['position'].initial = position.id
            else:
                fields['position'].help_text = self.other_help_text + title

        init_detail = self._init_detail_field
        init_detail(contents.get('tel'), self.phone_dict)
        init_detail(contents.get('email'), self.email_dict)
        init_detail(contents.get('url'), self.url_dict)

    def _init_detail_field(self, detail_data, field_dict):
        if detail_data:
            fields = self.fields

            for key in detail_data:
                param = key.params.get('TYPE')

                if param:
                    try:
                        fields[field_dict[param[0]]].initial = key.value
                    except KeyError:  # eg: invalid type, hidden field
                        pass
                else:
                    self._generate_help_text(field_dict['HOME'], key.value)

    def _init_orga_field(self, vcf_data):
        if vcf_data.contents.get('org'):
            fields = self.fields

            org_name = vcf_data.org.value[0]
            orga = Organisation.objects.filter(name=org_name).first()

            if orga:
                fields['organisation'].initial = orga.id
                fields['create_or_attach_orga'].initial = True

            fields['work_name'].initial = org_name
            fields['workaddr_name'].initial = org_name

    def _init_addresses_fields(self, vcf_data):
        fields = self.fields
        get_prefix = self.address_prefixes.get

        for adr in vcf_data.contents.get('adr', ()):
            param = adr.params.get('TYPE')
            value = adr.value

            if param:
                prefix = get_prefix(param[0])

                if prefix is None:
                    continue

                box = value.box
                fields[prefix + 'address'].initial = (
                    box + ' ' + value.street) if box else value.street
                fields[prefix + 'city'].initial = value.city
                fields[prefix + 'country'].initial = value.country
                fields[prefix + 'code'].initial = value.code
                fields[prefix + 'region'].initial = value.region
            else:
                self._generate_help_text(
                    'homeaddr_address',
                    ', '.join([
                        value.box,
                        value.street,
                        value.city,
                        value.region,
                        value.code,
                        value.country,
                    ]),
                )

    def _generate_help_text(self, field_name, value):
        field = self.fields[field_name]
        help_text = field.help_text

        if not help_text:
            field.help_text = self.type_help_text + value
        else:
            field.help_text = '{} | {}'.format(help_text, value)

    def _clean_orga_field(self, field_name):
        cleaned_data = self.cleaned_data
        cleaned = cleaned_data.get(field_name)

        if cleaned_data['create_or_attach_orga'] and not cleaned:
            raise ValidationError(
                self.error_messages['required4orga'],
                code='required4orga',
            )

        return cleaned

    clean_work_name = lambda self: self._clean_orga_field('work_name')
    clean_relation = lambda self: self._clean_orga_field('relation')

    def _clean_update_checkbox(self, checkbox_name):
        cleaned_data = self.cleaned_data
        checked = cleaned_data[checkbox_name]

        if checked:
            if not cleaned_data['create_or_attach_orga']:
                raise ValidationError(
                    self.error_messages['no_orga_creation'],
                    code='no_orga_creation',
                )
            elif not cleaned_data['organisation']:
                raise ValidationError(
                    self.error_messages['orga_not_selected'],
                    code='orga_not_selected',
                )

        return checked

    clean_update_work_name = lambda self: self._clean_update_checkbox(
        'update_work_name')
    clean_update_work_phone = lambda self: self._clean_update_checkbox(
        'update_work_phone')
    clean_update_work_email = lambda self: self._clean_update_checkbox(
        'update_work_email')
    clean_update_work_fax = lambda self: self._clean_update_checkbox(
        'update_work_fax')
    clean_update_work_url_site = lambda self: self._clean_update_checkbox(
        'update_work_url_site')
    clean_update_work_address = lambda self: self._clean_update_checkbox(
        'update_work_address')

    def clean_update_field(self, field_name):
        cleaned_data = self.cleaned_data
        value = cleaned_data[field_name]

        if not value and \
           all(cleaned_data[k] for k in ('create_or_attach_orga', 'organisation', 'update_' + field_name)):
            raise ValidationError(self.error_messages['required2update'],
                                  code='required2update')

        return value

    clean_work_phone = lambda self: self.clean_update_field('work_phone')
    clean_work_email = lambda self: self.clean_update_field('work_email')
    clean_work_fax = lambda self: self.clean_update_field('work_fax')
    clean_work_url_site = lambda self: self.clean_update_field('work_url_site')
    clean_work_address = lambda self: self.clean_update_field('work_address')

    def _create_contact(self, cleaned_data):
        get_data = cleaned_data.get

        return Contact.objects.create(
            user=cleaned_data['user'],
            civility=cleaned_data['civility'],
            first_name=cleaned_data['first_name'],
            last_name=cleaned_data['last_name'],
            position=get_data('position'),
            # NB: we do not use cleaned_data.get() in order to not overload
            #     default fields values
            **{
                fname: cleaned_data[fname]
                for fname in self.contact_details if fname in cleaned_data
            })

    def _create_address(self, cleaned_data, owner, data_prefix):
        # NB: we do not use cleaned_data.get() in order to not overload default fields values
        kwargs = {}
        for form_fname, model_fname in self.address_mapping:
            try:
                kwargs[model_fname] = cleaned_data[data_prefix + form_fname]
            except KeyError:
                pass

        address = Address(owner=owner, **kwargs)

        if address:
            address.save()
            return address

    def _create_image(self, contact):
        cleaned_data = self.cleaned_data
        image_encoded = cleaned_data['image_encoded']

        if image_encoded:
            img_name = secure_filename('{}_{}_{}'.format(
                contact.last_name, contact.first_name, contact.id))
            img_path = None

            if image_encoded.startswith(URL_START):
                tmp_img_path = None

                try:
                    if int(urlopen(image_encoded).info()
                           ['content-length']) <= settings.VCF_IMAGE_MAX_SIZE:
                        tmp_img_path = path.normpath(
                            path.join(IMG_UPLOAD_PATH, img_name))

                        urlretrieve(
                            image_encoded,
                            path.normpath(
                                path.join(settings.MEDIA_ROOT, tmp_img_path)))
                except:
                    logger.exception('Error with image')
                else:
                    img_path = tmp_img_path
            else:  # TODO: manage urls encoded in base64 ??
                try:
                    # TODO: factorise with activesync ??
                    img_data = base64.decodebytes(image_encoded.encode())
                    img_path = handle_uploaded_file(
                        ContentFile(img_data),
                        path=IMG_UPLOAD_PATH.split('/'),
                        name='{}.{}'.format(
                            img_name,
                            get_image_format(img_data),
                        ),
                    )
                except Exception:
                    logger.exception('VcfImportForm.save()')

            if img_path:
                return Document.objects.create(
                    user=cleaned_data['user'],
                    title=gettext('Image of {contact}').format(
                        contact=contact),
                    filedata=img_path,
                    linked_folder=Folder.objects.get(uuid=UUID_FOLDER_IMAGES),
                    description=gettext('Imported by VCFs'),
                )

    def _create_orga(self, contact):
        cleaned_data = self.cleaned_data

        if cleaned_data['create_or_attach_orga']:
            get_data = cleaned_data.get
            organisation = get_data('organisation')
            save_orga = False
            user = cleaned_data['user']
            addr_prefix = WORK_ADDR_PREFIX

            if organisation:
                # TODO: select_for_update() option in CreatorEntityField ?
                organisation = Organisation.objects.select_for_update().get(
                    id=organisation.id)

                for fname in self.orga_fields:
                    if get_data('update_work_' + fname):
                        setattr(organisation, fname, get_data('work_' + fname))

                if get_data('update_work_address'):
                    billing_address = organisation.billing_address

                    if billing_address is not None:
                        for form_fname, model_fname in self.address_mapping:
                            value = get_data(addr_prefix + form_fname)

                            if value:
                                setattr(billing_address, model_fname, value)

                        organisation.billing_address.save()
                    else:
                        organisation.billing_address = self._create_address(
                            cleaned_data,
                            owner=organisation,
                            data_prefix=addr_prefix,
                        )

                save_orga = True
            else:
                # NB: we do not use cleaned_data.get() in order to not overload default fields values
                orga_kwargs = {}
                for fname in self.orga_fields:
                    try:
                        orga_kwargs[fname] = cleaned_data['work_' + fname]
                    except KeyError:
                        pass

                organisation = Organisation.objects.create(user=user,
                                                           **orga_kwargs)

                orga_addr = self._create_address(
                    cleaned_data,
                    owner=organisation,
                    data_prefix=addr_prefix,
                )
                if orga_addr is not None:
                    organisation.billing_address = orga_addr
                    save_orga = True

            if save_orga:
                organisation.save()

            Relation.objects.create(
                user=user,
                subject_entity=contact,
                type=cleaned_data['relation'],
                object_entity=organisation,
            )

    @atomic
    def save(self, *args, **kwargs):
        cleaned_data = self.cleaned_data
        save_contact = False
        contact = self._create_contact(cleaned_data)

        image = self._create_image(contact)
        if image is not None:
            contact.image = image
            save_contact = True

        contact_addr = self._create_address(
            cleaned_data,
            owner=contact,
            data_prefix=HOME_ADDR_PREFIX,
        )
        if contact_addr is not None:
            contact.billing_address = contact_addr
            save_contact = True

        self._create_orga(contact)

        if save_contact:
            contact.save()

        return contact