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', }), }
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', }), }
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', )
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', }), }