class Form(BaseTriggerForm): channel = TembaChoiceField( Channel.objects.none(), label=_("Channel"), required=False, help_text= _("The channel to apply this trigger to, leave blank for all Facebook channels" ), ) referrer_id = forms.CharField( max_length=255, required=False, label=_("Referrer Id"), help_text=_("The referrer id that will trigger us")) def __init__(self, user, *args, **kwargs): super().__init__(user, Trigger.TYPE_REFERRAL, *args, **kwargs) self.fields["channel"].queryset = self.get_channel_choices( ContactURN.SCHEMES_SUPPORTING_REFERRALS) def get_conflicts_kwargs(self, cleaned_data): kwargs = super().get_conflicts_kwargs(cleaned_data) kwargs["channel"] = cleaned_data.get("channel") kwargs["referrer_id"] = cleaned_data.get("referrer_id", "").strip() return kwargs class Meta(BaseTriggerForm.Meta): fields = ("channel", "referrer_id") + BaseTriggerForm.Meta.fields
class CampaignForm(forms.ModelForm): group = TembaChoiceField( queryset=ContactGroup.user_groups.none(), empty_label=None, widget=SelectWidget(attrs={ "placeholder": _("Select group"), "searchable": True }), label=_("Group"), help_text= _("Only contacts in this group will be included in this campaign's events." ), ) def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["group"].queryset = ContactGroup.get_user_groups( user.get_org(), ready_only=False) class Meta: model = Campaign fields = ("name", "group") labels = {"name": _("Name")} widgets = {"name": InputWidget()}
class TestMessageForm(forms.Form): channel = TembaChoiceField(Channel.objects.filter(id__lt=0), help_text=_("Which channel will deliver the message")) urn = forms.CharField(max_length=14, help_text=_("The URN of the contact delivering this message")) text = forms.CharField(max_length=160, widget=forms.Textarea, help_text=_("The message that is being delivered")) def __init__(self, *args, **kwargs): # pragma: needs cover org = kwargs["org"] del kwargs["org"] super().__init__(*args, **kwargs) self.fields["channel"].queryset = Channel.objects.filter(org=org, is_active=True)
class Form(BaseTriggerForm): channel = TembaChoiceField(Channel.objects.none(), label=_("Channel"), required=True) def __init__(self, user, *args, **kwargs): super().__init__(user, Trigger.TYPE_NEW_CONVERSATION, *args, **kwargs) self.fields["channel"].queryset = self.get_channel_choices(ContactURN.SCHEMES_SUPPORTING_NEW_CONVERSATION) def get_conflicts_kwargs(self, cleaned_data): kwargs = super().get_conflicts_kwargs(cleaned_data) kwargs["channel"] = cleaned_data.get("channel") return kwargs class Meta(BaseTriggerForm.Meta): fields = ("channel",) + BaseTriggerForm.Meta.fields
class BaseTriggerForm(forms.ModelForm): """ Base form for different trigger types """ flow = TembaChoiceField( Flow.objects.none(), label=_("Flow"), required=True, widget=SelectWidget(attrs={ "placeholder": _("Select a flow"), "searchable": True }), ) groups = TembaMultipleChoiceField( queryset=ContactGroup.user_groups.none(), label=_("Groups To Include"), help_text=_("Only includes contacts in these groups."), required=False, widget=SelectMultipleWidget( attrs={ "icons": True, "placeholder": _("Optional: Select contact groups"), "searchable": True }), ) exclude_groups = TembaMultipleChoiceField( queryset=ContactGroup.user_groups.none(), label=_("Groups To Exclude"), help_text=_("Excludes contacts in these groups."), required=False, widget=SelectMultipleWidget( attrs={ "icons": True, "placeholder": _("Optional: Select contact groups"), "searchable": True }), ) def __init__(self, user, trigger_type, *args, **kwargs): super().__init__(*args, **kwargs) self.user = user self.org = user.get_org() self.trigger_type = Trigger.get_type(code=trigger_type) flow_types = self.trigger_type.allowed_flow_types flows = self.org.flows.filter(flow_type__in=flow_types, is_active=True, is_archived=False, is_system=False) self.fields["flow"].queryset = flows.order_by("name") groups = ContactGroup.get_user_groups(self.org, ready_only=False) self.fields["groups"].queryset = groups self.fields["exclude_groups"].queryset = groups def get_channel_choices(self, schemes): return self.org.channels.filter( is_active=True, schemes__overlap=list(schemes)).order_by("name") def get_conflicts(self, cleaned_data): conflicts = Trigger.get_conflicts( self.org, self.trigger_type.code, **self.get_conflicts_kwargs(cleaned_data)) # if we're editing a trigger we can't conflict with ourselves if self.instance: conflicts = conflicts.exclude(id=self.instance.id) return conflicts def get_conflicts_kwargs(self, cleaned_data): return {"groups": cleaned_data.get("groups", [])} def clean_keyword(self): keyword = self.cleaned_data.get("keyword") or "" keyword = keyword.strip() if not self.trigger_type.is_valid_keyword(keyword): raise forms.ValidationError( _("Must be a single word containing only letters and numbers, or a single emoji character." )) return keyword.lower() def clean(self): cleaned_data = super().clean() groups = cleaned_data.get("groups", []) exclude_groups = cleaned_data.get("exclude_groups", []) if set(groups).intersection(exclude_groups): raise forms.ValidationError( _("Can't include and exclude the same group.")) # only check for conflicts if user is submitting valid data for all fields if not self.errors and self.get_conflicts(cleaned_data): raise forms.ValidationError( _("There already exists a trigger of this type with these options." )) return cleaned_data class Meta: model = Trigger fields = ("flow", "groups", "exclude_groups")
class CampaignEventForm(forms.ModelForm): event_type = forms.ChoiceField( choices=((CampaignEvent.TYPE_MESSAGE, "Send a message"), (CampaignEvent.TYPE_FLOW, "Start a flow")), required=True, widget=SelectWidget(attrs={"placeholder": _("Select the event type"), "widget_only": True}), ) direction = forms.ChoiceField( choices=(("B", "Before"), ("A", "After")), required=True, widget=SelectWidget(attrs={"placeholder": _("Relative date direction"), "widget_only": True}), ) unit = forms.ChoiceField( choices=CampaignEvent.UNIT_CHOICES, required=True, widget=SelectWidget(attrs={"placeholder": _("Select a unit"), "widget_only": True}), ) flow_to_start = TembaChoiceField( queryset=Flow.objects.filter(is_active=True), required=False, empty_label=None, widget=SelectWidget( attrs={ "placeholder": _("Select a flow to start"), "widget_only": True, "searchable": True, } ), ) relative_to = TembaChoiceField( queryset=ContactField.all_fields.none(), required=False, empty_label=None, widget=SelectWidget( attrs={ "placeholder": _("Select a date field to base this event on"), "widget_only": True, "searchable": True, } ), ) delivery_hour = forms.ChoiceField( choices=CampaignEvent.get_hour_choices(), required=False, widget=SelectWidget(attrs={"placeholder": _("Select hour for delivery"), "widget_only": True}), ) flow_start_mode = forms.ChoiceField( choices=( (CampaignEvent.MODE_INTERRUPT, _("Stop it and run this flow")), (CampaignEvent.MODE_SKIP, _("Skip this event")), ), required=False, widget=SelectWidget(attrs={"widget_only": True}), ) message_start_mode = forms.ChoiceField( choices=( (CampaignEvent.MODE_INTERRUPT, _("Stop it and send the message")), (CampaignEvent.MODE_SKIP, _("Skip this message")), (CampaignEvent.MODE_PASSIVE, _("Send the message")), ), required=False, widget=SelectWidget(attrs={"widget_only": True}), ) def clean(self): data = super().clean() if self.data["event_type"] == CampaignEvent.TYPE_MESSAGE: if self.languages: language = self.languages[0].language iso_code = language["iso_code"] if iso_code not in self.data or not self.data[iso_code].strip(): raise ValidationError(_("A message is required for '%s'") % language["name"]) for lang_data in self.languages: lang = lang_data.language iso_code = lang["iso_code"] if iso_code in self.data and len(self.data[iso_code].strip()) > Msg.MAX_TEXT_LEN: raise ValidationError( _("Translation for '%(language)s' exceeds the %(limit)d character limit.") % dict(language=lang["name"], limit=Msg.MAX_TEXT_LEN) ) if not data.get("message_start_mode"): self.add_error("message_start_mode", _("This field is required.")) else: if not data.get("flow_to_start"): self.add_error("flow_to_start", _("This field is required.")) if not data.get("flow_start_mode"): self.add_error("flow_start_mode", _("This field is required.")) return data def pre_save(self, request, obj): org = self.user.get_org() # if it's before, negate the offset if self.cleaned_data["direction"] == "B": obj.offset = -obj.offset if self.cleaned_data["unit"] == "H" or self.cleaned_data["unit"] == "M": # pragma: needs cover obj.delivery_hour = -1 # if its a message flow, set that accordingly if self.cleaned_data["event_type"] == CampaignEvent.TYPE_MESSAGE: if self.instance.id: base_language = self.instance.flow.base_language else: base_language = org.flow_languages[0] if org.flow_languages else "base" translations = {} for language in self.languages: iso_code = language.language["iso_code"] if iso_code in self.cleaned_data and self.cleaned_data.get(iso_code, "").strip(): translations[iso_code] = self.cleaned_data.get(iso_code, "").strip() if not obj.flow_id or not obj.flow.is_active or not obj.flow.is_system: obj.flow = Flow.create_single_message(org, request.user, translations, base_language=base_language) else: # set our single message on our flow obj.flow.update_single_message_flow(self.user, translations, base_language) obj.message = translations obj.full_clean() obj.start_mode = self.cleaned_data["message_start_mode"] # otherwise, it's an event that runs an existing flow else: obj.flow = self.cleaned_data["flow_to_start"] obj.start_mode = self.cleaned_data["flow_start_mode"] # force passive mode for user-selected background flows if obj.flow.flow_type == Flow.TYPE_BACKGROUND: obj.start_mode = CampaignEvent.MODE_PASSIVE def __init__(self, user, event, *args, **kwargs): self.user = user super().__init__(*args, **kwargs) org = self.user.get_org() relative_to = self.fields["relative_to"] relative_to.queryset = ContactField.all_fields.filter( org=org, is_active=True, value_type=ContactField.TYPE_DATETIME ).order_by("label") flow = self.fields["flow_to_start"] flow.queryset = Flow.objects.filter( org=self.user.get_org(), flow_type__in=[Flow.TYPE_MESSAGE, Flow.TYPE_VOICE, Flow.TYPE_BACKGROUND], is_active=True, is_archived=False, is_system=False, ).order_by("name") if event and event.flow and event.flow.flow_type == Flow.TYPE_BACKGROUND: flow.widget.attrs["info_text"] = CampaignEventCRUDL.BACKGROUND_WARNING message = self.instance.message or {} self.languages = [] # add in all of our languages for message forms for lang_code in org.flow_languages: lang_name = languages.get_name(lang_code) insert = None # if it's our primary language, allow use to steal the 'base' message if org.flow_languages and org.flow_languages[0] == lang_code: initial = message.get(lang_code, "") if not initial: initial = message.get("base", "") # also, let's show it first insert = 0 else: # otherwise, its just a normal language initial = message.get(lang_code, "") field = forms.CharField( widget=CompletionTextarea( attrs={ "placeholder": _( "Hi @contact.name! This is just a friendly reminder to apply your fertilizer." ), "widget_only": True, } ), required=False, label=lang_name, initial=initial, ) self.fields[lang_code] = field field.language = dict(name=lang_name, iso_code=lang_code) # see if we need to insert or append if insert is not None: self.languages.insert(insert, field) else: self.languages.append(field) # determine our base language if necessary base_language = org.flow_languages[0] if org.flow_languages else "base" # if we are editing, always include the flow base language if self.instance.id: base_language = self.instance.flow.base_language # add our default language, we'll insert it at the front of the list if base_language and base_language not in self.fields: field = forms.CharField( widget=CompletionTextarea( attrs={ "placeholder": _( "Hi @contact.name! This is just a friendly reminder to apply your fertilizer." ), "widget_only": True, } ), required=False, label=_("Default"), initial=message.get(base_language), ) self.fields[base_language] = field field.language = dict(iso_code=base_language, name="Default") self.languages.insert(0, field) class Meta: model = CampaignEvent fields = "__all__" widgets = {"offset": InputWidget(attrs={"widget_only": True})}