Exemple #1
0
class CreateUpdateCaseForm(TagsFormMixin, HelloLilyModelForm):
    """
    Form for adding or editing a case.
    """
    status = forms.ModelChoiceField(
        label=_('Status'),
        queryset=CaseStatus.objects,
        empty_label=_('Select a status'),
    )

    type = forms.ModelChoiceField(
        label=_('Type'),
        queryset=CaseType.objects,
        empty_label=_('Select a type'),
        required=True,
    )

    account = forms.ModelChoiceField(
        label=_('Account'),
        required=False,
        queryset=Account.objects,
        empty_label=_('Select an account'),
        widget=AjaxSelect2Widget(url=reverse_lazy('search_view'),
                                 model=Account,
                                 filter_on='%s,id_contact' %
                                 AccountMapping.get_mapping_type_name()),
    )

    contact = forms.ModelChoiceField(
        label=_('Contact'),
        required=False,
        queryset=Contact.objects,
        empty_label=_('Select a contact'),
        widget=AjaxSelect2Widget(url=reverse_lazy('search_view'),
                                 model=Contact,
                                 filter_on='%s,id_account' %
                                 ContactMapping.get_mapping_type_name()),
    )

    assigned_to_groups = forms.ModelMultipleChoiceField(
        label=_('Assigned to teams'),
        required=False,
        queryset=LilyGroup.objects,
        help_text='',
        widget=forms.SelectMultiple(
            attrs={
                'placeholder': _('Select one or more team(s)'),
            }))

    assigned_to = forms.ModelChoiceField(label=_('Assigned to'),
                                         queryset=LilyUser.objects,
                                         empty_label=_('Select a user'),
                                         required=False)

    expires = forms.DateField(label=_('Expires'),
                              input_formats=settings.DATE_INPUT_FORMATS,
                              widget=DatePicker(
                                  options={
                                      'autoclose': 'true',
                                  },
                                  format=settings.DATE_INPUT_FORMATS[0],
                                  attrs={
                                      'placeholder':
                                      DatePicker.conv_datetime_format_py2js(
                                          settings.DATE_INPUT_FORMATS[0]),
                                  },
                              ))

    parcel_provider = forms.ChoiceField(
        choices=Parcel.PROVIDERS,
        required=False,
        label=_('Parcel provider'),
    )

    parcel_identifier = forms.CharField(max_length=255,
                                        required=False,
                                        label=_('Parcel identifier'))

    is_archived = forms.BooleanField(widget=forms.HiddenInput(),
                                     required=False)

    def __init__(self, *args, **kwargs):
        """
        Set queryset and initial for *assign_to*
        """
        super(CreateUpdateCaseForm, self).__init__(*args, **kwargs)

        # FIXME: WORKAROUND FOR TENANT FILTER.
        # An error will occur when using LilyUser.objects.all(), most likely because
        # the foreign key to contact (and maybe account) is filtered and executed before
        # the filter for the LilyUser. This way it's possible contacts (and maybe accounts)
        # won't be found for a user. But since it's a required field, an exception is raised.
        user = get_current_user()
        self.fields['assigned_to'].queryset = LilyUser.objects.filter(
            tenant=user.tenant)
        self.fields['expires'].initial = datetime.today()

        # Pre-select users team if possible
        groups = user.lily_groups.all()
        if len(groups) == 1:
            self.fields['assigned_to_groups'].initial = groups

        # Setup parcel initial values
        self.fields['parcel_provider'].initial = Parcel.DPD
        self.fields['status'].initial = CaseStatus.objects.first()

        self.fields['type'].queryset = CaseType.objects.filter(
            is_archived=False)

        if self.instance.parcel is not None:
            self.fields[
                'parcel_provider'].initial = self.instance.parcel.provider
            self.fields[
                'parcel_identifier'].initial = self.instance.parcel.identifier

    def clean(self):
        """
        Form validation: all fields should be unique.
        """
        cleaned_data = super(CreateUpdateCaseForm, self).clean()

        contact = cleaned_data.get('contact')
        account = cleaned_data.get('account')

        # Check if a contact or an account has been selected
        if not contact and not account:
            self._errors['contact'] = self._errors[
                'account'] = self.error_class(
                    [_('Select a contact or account')])

        # Verify that a contact works at selected account
        if contact and account and not account.functions.filter(
                contact__id=contact.id).exists():
            self._errors['contact'] = self._errors[
                'account'] = self.error_class(
                    [_('Selected contact doesn\'t work at account')])

        return cleaned_data

    def save(self, commit=True):
        """
        Check for parcel information and store in separate model
        """
        if not self.instance.id:
            self.instance.created_by = get_current_user()

        instance = super(CreateUpdateCaseForm, self).save(commit=commit)

        # Add parcel information
        if self.cleaned_data['parcel_identifier'] and self.cleaned_data[
                'parcel_provider']:
            # There is parcel information stored
            if instance.parcel:
                # Update
                instance.parcel.provider = self.cleaned_data['parcel_provider']
                instance.parcel.identifier = self.cleaned_data[
                    'parcel_identifier']
            else:
                # Create
                parcel = Parcel(
                    provider=self.cleaned_data['parcel_provider'],
                    identifier=self.cleaned_data['parcel_identifier'])
                if commit:
                    parcel.save()
                instance.parcel = parcel
        elif instance.parcel:
            # Remove parcel
            instance.parcel = None

        # If archived, set status to last position
        if instance.is_archived:
            instance.status = CaseStatus.objects.last()

        if commit:
            instance.save()

        return instance

    class Meta:
        model = Case
        fieldsets = (
            (_('Who was it?'), {
                'fields': (
                    'account',
                    'contact',
                ),
            }),
            (_('What to do?'), {
                'fields': (
                    'subject',
                    'description',
                    'type',
                ),
            }),
            (_('Who is going to do this?'), {
                'fields': (
                    'assigned_to_groups',
                    'assigned_to',
                ),
            }),
            (_('When to do it?'), {
                'fields': (
                    'status',
                    'priority',
                    'expires',
                    'is_archived',
                ),
            }),
            (_('Parcel information'), {
                'fields': (
                    'parcel_provider',
                    'parcel_identifier',
                )
            }),
        )

        widgets = {
            'priority':
            PrioritySelect(attrs={
                'class': 'chzn-select-no-search',
                'update-case-expire-date': '',
            }),
            'description':
            forms.Textarea({'rows': 3}),
            'status':
            forms.Select(attrs={
                'class': 'chzn-select-no-search',
            }),
        }
Exemple #2
0
class CreateUpdateDealForm(TagsFormMixin, HelloLilyModelForm):
    """
    Form for adding or editing a deal.
    """
    account = forms.ModelChoiceField(
        label=_('Account'),
        queryset=Account.objects,
        empty_label=_('Select an account'),
        widget=AjaxSelect2Widget(
            url=reverse_lazy('search_view'),
            filter_on=AccountMapping.get_mapping_type_name(),
            model=Account,
        ),
    )

    assigned_to = forms.ModelChoiceField(
        label=_('Assigned to'),
        queryset=LilyUser.objects,
        empty_label=None,
        widget=forms.Select()
    )

    expected_closing_date = forms.DateField(
        label=_('Expected closing date'),
        input_formats=settings.DATE_INPUT_FORMATS,
        initial=add_business_days(datetime.datetime.now(), 12),
        widget=DatePicker(
            options={
                'autoclose': 'true',
            },
            format=settings.DATE_INPUT_FORMATS[0],
            attrs={
                'placeholder': DatePicker.conv_datetime_format_py2js(settings.DATE_INPUT_FORMATS[0]),
            },
        )
    )

    is_archived = forms.BooleanField(widget=forms.HiddenInput(), required=False)

    def __init__(self, *args, **kwargs):
        """
        Overloading super().__init__() to make accounts available as assignees
        """
        super(CreateUpdateDealForm, self).__init__(*args, **kwargs)

        # FIXME: WORKAROUND FOR TENANT FILTER.
        # An error will occur when using LilyUser.objects.all(), most likely because
        # the foreign key to contact (and maybe account) is filtered and executed before
        # the filter for the LilyUser. This way it's possible contacts (and maybe accounts)
        # won't be found for a user. But since it's a required field, an exception is raised.
        self.fields['assigned_to'].queryset = LilyUser.objects.filter(tenant=get_current_user().tenant)
        self.fields['assigned_to'].initial = get_current_user()

    def save(self, commit=True):
        instance = super(CreateUpdateDealForm, self).save(commit=commit)

        # Set closed_date after changing stage to lost/won and reset it when it's new/pending
        if instance.stage in [Deal.WON_STAGE, Deal.LOST_STAGE]:
            instance.closed_date = datetime.datetime.utcnow().replace(tzinfo=utc)
        else:
            instance.closed_date = None

        if commit:
            instance.save()
        return instance

    class Meta:
        model = Deal

        fieldsets = (
            (_('Who is it?'), {
                'fields': ('account', 'is_archived', 'new_business', 'found_through', 'contacted_by'),
            }),
            (_('What is it?'), {
                'fields': ('name', 'amount_once', 'amount_recurring', 'currency', 'description', 'quote_id'),
            }),
            (_('What\'s the status?'), {
                'fields': ('stage', 'expected_closing_date', 'assigned_to'),
            }),
            (_('Action checklist'), {
                'fields': ('twitter_checked', 'is_checked', 'card_sent', 'feedback_form_sent'),
            }),
        )

        widgets = {
            'description': forms.Textarea({
                'rows': 3
            }),
            'currency': forms.Select(attrs={
                'class': 'chzn-select-no-search',
            }),
            'stage': forms.Select(attrs={
                'class': 'chzn-select-no-search',
            }),
            'feedback_form_sent': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
            'new_business': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
            'is_checked': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
            'twitter_checked': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
            'card_sent': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
        }
Exemple #3
0
class ComposeEmailForm(FormSetFormMixin, HelloLilyForm):
    """
    Form for writing an EmailMessage as a draft, reply or forwarded message.
    """
    draft_pk = forms.IntegerField(widget=forms.HiddenInput(), required=False)

    template = forms.ModelChoiceField(
        label=_('Template'),
        queryset=EmailTemplate.objects,
        empty_label=_('Choose a template'),
        required=False
    )

    send_from = forms.ChoiceField()

    send_to_normal = TagsField(
        label=_('To'),
        widget=AjaxSelect2Widget(
            attrs={
                'class': 'tags-ajax'
            },
            url=reverse_lazy('search_view'),
            model=Contact,
            filter_on='contacts_contact',
        ),
    )
    send_to_cc = TagsField(
        label=_('Cc'),
        required=False,
        widget=AjaxSelect2Widget(
            attrs={
                'class': 'tags-ajax'
            },
            url=reverse_lazy('search_view'),
            model=Contact,
            filter_on='contacts_contact',
        ),
    )
    send_to_bcc = TagsField(
        label=_('Bcc'),
        required=False,
        widget=AjaxSelect2Widget(
            attrs={
                'class': 'tags-ajax'
            },
            url=reverse_lazy('search_view'),
            model=Contact,
            filter_on='contacts_contact',
        ),
    )

    attachments = FormSetField(
        queryset=EmailOutboxAttachment.objects,
        formset_class=modelformset_factory(EmailAttachment, form=AttachmentBaseForm, can_delete=True, extra=0),
        template='email/formset_attachment.html',
    )

    subject = forms.CharField(required=False)
    body_html = forms.CharField(widget=Wysihtml5Input(), required=False)

    def __init__(self, *args, **kwargs):
        self.message_type = kwargs.pop('message_type', 'reply')
        super(ComposeEmailForm, self).__init__(*args, **kwargs)

        if 'initial' in kwargs and 'draft_pk' in kwargs['initial']:
            if self.message_type is not 'reply':
                self.initial['attachments'] = EmailAttachment.objects.filter(
                    message_id=kwargs['initial']['draft_pk'],
                    inline=False
                )

        self.fields['template'].queryset = EmailTemplate.objects.order_by('name')

        user = get_current_user()
        self.email_accounts = EmailAccount.objects.filter(
            Q(owner=user) |
            Q(shared_with_users=user) |
            Q(public=True)
        ).filter(tenant=user.tenant, is_deleted=False).distinct('id')

        # Only provide choices you have access to
        self.fields['send_from'].choices = [(email_account.id, email_account) for email_account in self.email_accounts]
        self.fields['send_from'].empty_label = None

        # Set user's primary_email as default choice if there is no initial value
        initial_email_account = self.initial.get('send_from', None)
        if not initial_email_account:
            if user.primary_email_account:
                initial_email_account = user.primary_email_account.id
            else:
                for email_account in self.email_accounts:
                    if email_account.email_address == user.email:
                        initial_email_account = email_account
                        break
        elif isinstance(initial_email_account, basestring):
            for email_account in self.email_accounts:
                if email_account.email == initial_email_account:
                    initial_email_account = email_account
                    break

        self.initial['send_from'] = initial_email_account

    def is_multipart(self):
        """
        Return True since file uploads are possible.
        """
        return True

    def clean(self):
        cleaned_data = super(ComposeEmailForm, self).clean()

        # Make sure at least one of the send_to_X fields is filled in when sending the email
        if 'submit-send' in self.data:
            if not any([cleaned_data.get('send_to_normal'), cleaned_data.get('send_to_cc'), cleaned_data.get('send_to_bcc')]):
                self._errors['send_to_normal'] = self.error_class([_('Please provide at least one recipient.')])

        # Clean send_to addresses.
        cleaned_data['send_to_normal'] = self.format_recipients(cleaned_data.get('send_to_normal'))
        cleaned_data['send_to_cc'] = self.format_recipients(cleaned_data.get('send_to_cc'))
        cleaned_data['send_to_bcc'] = self.format_recipients(cleaned_data.get('send_to_bcc'))

        for recipient in self.cleaned_data['send_to_normal']:
            email = parseaddr(recipient)[1]
            validate_email(email)

        return cleaned_data

    def format_recipients(self, recipients):
        """
        Strips newlines and trailing spaces & commas from recipients.
        Args:
            recipients (str): The string that needs cleaning up.
        Returns:
            String of comma separated email addresses.
        """
        formatted_recipients = []
        for recipient in recipients:
            # Clean each part of the string
            formatted_recipients.append(recipient.rstrip(', ').strip())

        # Create one string from the parts
        formatted_recipients = ', '.join(formatted_recipients)

        # Regex to split a string by comma while ignoring commas in between quotes
        pattern = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')

        # Split the single string into separate recipients
        formatted_recipients = pattern.split(formatted_recipients)[1::2]

        # It's possible that an extra space is added, so strip it
        formatted_recipients = [recipient.strip() for recipient in formatted_recipients]

        return formatted_recipients

    def clean_send_from(self):
        """
        Verify send_from is a valid account the user has access to.
        """
        cleaned_data = self.cleaned_data
        send_from = cleaned_data.get('send_from')

        try:
            send_from = int(send_from)
        except ValueError:
            pass
        else:
            try:
                send_from = self.email_accounts.get(pk=send_from)
            except EmailAccount.DoesNotExist:
                raise ValidationError(
                    _('Invalid email account selected to use as sender.'),
                    code='invalid',
                )
            else:
                return send_from

    class Meta:
        fields = (
            'draft_pk',
            'send_from',
            'send_to_normal',
            'send_to_cc',
            'send_to_bcc',
            'subject',
            'template',
            'body_html',
            'attachments',
        )
Exemple #4
0
class CreateUpdateContactForm(FormSetFormMixin, TagsFormMixin):
    """
    Form to add a contact which all fields available.
    """
    account = forms.CharField(
        label=_('Works at'),
        required=False,
        help_text='',
        widget=AjaxSelect2Widget(
            tags=True,
            url=reverse_lazy('search_view'),
            filter_on=AccountMapping.get_mapping_type_name(),
            model=Account,
            attrs={
                'class': 'select2ajax',
                'placeholder': _('Select one or more account(s)'),
            }
        )
    )

    twitter = forms.CharField(
        label=_('Twitter'),
        required=False,
        widget=AddonTextInput(
            icon_attrs={
                'class': 'fa fa-twitter',
                'position': 'left',
                'is_button': False
            }
        )
    )

    linkedin = forms.CharField(
        label=_('LinkedIn'),
        required=False,
        widget=AddonTextInput(
            icon_attrs={
                'class': 'fa fa-linkedin',
                'position': 'left',
                'is_button': False
            }
        )
    )

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

        self.fields['account'].help_text = ''  # Fixed in django 1.8: now the help text is appended instead of overwritten

        if kwargs.get('initial', None):
            kwargs_initial = kwargs.get('initial', None)
            if kwargs_initial.get('account', None):
                self.fields['account'].initial = {kwargs_initial.get('account', None)}
                self.fields['account'].widget.data = self.fields['account'].initial

        if self.instance.pk:
            self.fields['account'].initial = [function.account for function in self.instance.functions.all()]
            self.fields['account'].widget.data = self.fields['account'].initial

            twitter = self.instance.social_media.filter(name='twitter').first()
            self.fields['twitter'].initial = twitter.username if twitter else ''

            linkedin = self.instance.social_media.filter(name='linkedin').first()
            self.fields['linkedin'].initial = linkedin.username if linkedin else ''

        self.fields['addresses'].form_attrs = {
            'exclude_address_types': ['visiting'],
            'extra_form_kwargs': {
                'initial': {
                    'type': 'home',
                    'country': 'NL',
                }
            }
        }

    def clean_twitter(self):
        """
        Check if added twitter name or url is valid.

        Returns:
            string: twitter username or empty string.
        """
        twitter = self.cleaned_data.get('twitter')

        if twitter:
            try:
                twit = Twitter(twitter)
            except ValueError:
                raise ValidationError(_('Please enter a valid username or link'), code='invalid')
            else:
                return twit.username
        return twitter

    def clean_linkedin(self):
        """
        Check if added linkedin url is a valid linkedin url.

        Returns:
            string: linkedin username or empty string.
        """
        linkedin = self.cleaned_data['linkedin']

        if linkedin:
            try:
                lin = LinkedIn(linkedin)
            except ValueError:
                raise ValidationError(_('Please enter a valid username or link'), code='invalid')
            else:
                return lin.username

        return linkedin

    def clean_account(self):
        account_ids = self.cleaned_data['account'].split(',')
        for i, account_id in enumerate(account_ids):
            try:
                account_ids[i] = int(account_id)
            except ValueError:
                account_ids[i] = 0

        return Account.objects.filter(pk__in=account_ids)

    def clean(self):
        """
        Form validation: fill in at least first or last name.

        Returns:
            dict: cleaned data of all fields.
        """
        cleaned_data = super(CreateUpdateContactForm, self).clean()

        # Check if at least first or last name has been provided.
        if not cleaned_data.get('first_name') and not cleaned_data.get('last_name'):
            self._errors['first_name'] = self._errors['last_name'] = self.error_class([_('Name can\'t be empty')])

        return cleaned_data

    def save(self, commit=True):
        """
        Save contact to instance, and to database if commit is True.

        Returns:
            instance: an instance of the contact model
        """
        instance = super(CreateUpdateContactForm, self).save(commit)

        if commit:
            account_input = self.cleaned_data.get('account')
            twitter_input = self.cleaned_data.get('twitter')
            linkedin_input = self.cleaned_data.get('linkedin')

            self.instance.functions.exclude(account__in=account_input).delete()
            function_list = [Function.objects.get_or_create(contact=self.instance, account=account)[0] for account in account_input]
            for function in function_list:
                self.instance.functions.add(function)

            if twitter_input and instance.social_media.filter(name='twitter').exists():
                # There is input and there are one or more twitters connected, so we get the first of those.
                twitter_queryset = self.instance.social_media.filter(name='twitter')
                if self.fields['twitter'].initial:  # Only filter on initial if there is initial data.
                    twitter_queryset = twitter_queryset.filter(username=self.fields['twitter'].initial)
                twitter_instance = twitter_queryset.first()

                # And we edit it to store our new input.
                twitter = Twitter(self.cleaned_data.get('twitter'))
                twitter_instance.username = twitter.username
                twitter_instance.profile_url = twitter.profile_url
                twitter_instance.save()
            elif twitter_input:
                # There is input but no connected twitter, so we create a new one.
                twitter = Twitter(self.cleaned_data.get('twitter'))
                twitter_instance = SocialMedia.objects.create(
                    name='twitter',
                    username=twitter.username,
                    profile_url=twitter.profile_url,
                )
                instance.social_media.add(twitter_instance)
            else:
                # There is no input and zero or more connected twitters, so we delete them all.
                instance.social_media.filter(name='twitter').delete()

            if linkedin_input and instance.social_media.filter(name='linkedin').exists():
                # There is input and there are one or more linkedins connected, so we get the first of those.
                linkedin_instance = self.instance.social_media.filter(name='linkedin')
                if self.fields['linkedin'].initial:  # Only filter on initial if there is initial data.
                    linkedin_instance = linkedin_instance.filter(username=self.fields['linkedin'].initial)
                linkedin_instance = linkedin_instance.first()

                # And we edit it to store our new input.
                linkedin = LinkedIn(self.cleaned_data.get('linkedin'))
                linkedin_instance.username = linkedin.username
                linkedin_instance.profile_url = linkedin.profile_url
                linkedin_instance.save()
            elif linkedin_input:
                # There is input but no connected linkedin, so we create a new one.
                linkedin = LinkedIn(self.cleaned_data.get('linkedin'))
                linkedin_instance = SocialMedia.objects.create(
                    name='linkedin',
                    username=linkedin.username,
                    profile_url=linkedin.profile_url,
                )
                instance.social_media.add(linkedin_instance)
            else:
                # There is no input and zero or more connected twitters, so we delete them all.
                instance.social_media.filter(name='linkedin').delete()

        return instance

    class Meta:
        model = Contact
        fields = (
            'salutation',
            'gender',
            'first_name',
            'preposition',
            'last_name',
            'account',
            'description',
            'email_addresses',
            'phone_numbers',
            'addresses',
        )
        widgets = {
            'description': ShowHideWidget(forms.Textarea(attrs={
                'rows': 3,
            })),
            'salutation': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
            'gender': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
        }