示例#1
0
class LocationReassignmentRequestForm(BulkUploadForm):
    VALIDATE = "validate"
    EMAIL_HOUSEHOLDS = "email_households"
    UPDATE = "update"
    REASSIGN_HOUSEHOLDS = "reassign_households"
    ACTION_TYPE_CHOICES = [(VALIDATE, ugettext("Validate")),
                           (EMAIL_HOUSEHOLDS, ugettext("Email Households")),
                           (UPDATE, ugettext("Perform Reassignment")),
                           (REASSIGN_HOUSEHOLDS,
                            ugettext("Reassign Households"))]
    action_type = forms.ChoiceField(
        choices=ACTION_TYPE_CHOICES,
        initial=VALIDATE,
        widget=SelectToggle(choices=ACTION_TYPE_CHOICES))

    def crispy_form_fields(self, context):
        crispy_form_fields = super(LocationReassignmentRequestForm,
                                   self).crispy_form_fields(context)
        crispy_form_fields.extend([
            crispy.Div(InlineField('action_type')),
        ])
        return crispy_form_fields
示例#2
0
class ScheduledReportForm(forms.Form):
    INTERVAL_CHOICES = [("daily", "Daily"), ("weekly", "Weekly"),
                        ("monthly", "Monthly")]

    config_ids = forms.MultipleChoiceField(
        label=_("Saved report(s)"),
        validators=[MinLengthValidator(1)],
        help_text='Note: not all built-in reports support email delivery, so'
        ' some of your saved reports may not appear in this list')

    interval = forms.TypedChoiceField(label=_('Interval'),
                                      widget=SelectToggle(
                                          choices=INTERVAL_CHOICES,
                                          apply_bindings=True),
                                      choices=INTERVAL_CHOICES)

    day = forms.TypedChoiceField(label=_("Day"),
                                 coerce=int,
                                 required=False,
                                 choices=[(i, i) for i in range(0, 32)])

    hour = forms.TypedChoiceField(label=_('Time'),
                                  coerce=int,
                                  choices=ReportNotification.hour_choices())

    start_date = forms.DateField(label=_('Report Start Date'), required=False)

    send_to_owner = forms.BooleanField(label=_('Send to owner'),
                                       required=False)

    attach_excel = forms.BooleanField(label=_('Attach Excel Report'),
                                      required=False)

    recipient_emails = MultiEmailField(label=_('Other recipients'),
                                       required=False)
    email_subject = forms.CharField(
        required=False,
        help_text=
        'Translated into recipient\'s language if set to "%(default_subject)s".'
        % {
            'default_subject': DEFAULT_REPORT_NOTIF_SUBJECT,
        },
    )

    language = forms.ChoiceField(label=_('Language'),
                                 required=False,
                                 choices=[('', '')] +
                                 langcodes.get_all_langs_for_select(),
                                 widget=forms.Select())

    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_class = 'form-horizontal'
        self.helper.form_id = 'id-scheduledReportForm'
        self.helper.label_class = 'col-sm-3 col-md-2'
        self.helper.field_class = 'col-sm-9 col-md-8 col-lg-6'
        self.helper.add_layout(
            crispy.Layout(
                crispy.Fieldset(
                    ugettext("Configure Scheduled Report"), 'config_ids',
                    'interval', 'day', 'hour', 'start_date',
                    crispy.Field(
                        'email_subject',
                        css_class='input-xlarge',
                    ), crispy.Field('send_to_owner'),
                    crispy.Field('attach_excel'), 'recipient_emails',
                    'language',
                    crispy.HTML(
                        render_to_string(
                            'reports/partials/privacy_disclaimer.html'))),
                FormActions(crispy.Submit('submit_btn', 'Submit'))))

        super(ScheduledReportForm, self).__init__(*args, **kwargs)

    def clean(self):
        cleaned_data = super(ScheduledReportForm, self).clean()
        if cleaned_data["interval"] == "daily":
            del cleaned_data["day"]
        _verify_email(cleaned_data)
        return cleaned_data
示例#3
0
class SettingsForm(Form):
    # General Settings
    use_default_sms_response = ChoiceField(
        required=False,
        label=ugettext_noop("Default SMS Response"),
        choices=ENABLED_DISABLED_CHOICES,
    )
    default_sms_response = TrimmedCharField(
        required=False,
        label="",
    )
    use_restricted_sms_times = ChoiceField(
        required=False,
        label=ugettext_noop("Send SMS on..."),
        choices=(
            (DISABLED, ugettext_noop("any day, at any time")),
            (ENABLED, ugettext_noop("only specific days and times")),
        ),
    )
    restricted_sms_times_json = CharField(
        required=False,
        widget=forms.HiddenInput,
    )

    sms_survey_date_format = ChoiceField(
        required=False,
        label=ugettext_lazy("SMS Survey Date Format"),
        choices=((df.human_readable_format,
                  ugettext_lazy(df.human_readable_format))
                 for df in ALLOWED_SURVEY_DATE_FORMATS),
    )

    # Chat Settings
    use_custom_case_username = ChoiceField(
        required=False,
        choices=DEFAULT_CUSTOM_CHOICES,
    )
    custom_case_username = TrimmedCharField(
        required=False,
        label=ugettext_noop("Enter a Case Property"),
    )
    use_custom_message_count_threshold = ChoiceField(
        required=False,
        choices=MESSAGE_COUNTER_CHOICES,
    )
    custom_message_count_threshold = IntegerField(
        required=False,
        label=ugettext_noop("Enter a Number"),
    )
    use_sms_conversation_times = ChoiceField(
        required=False,
        label=ugettext_noop("Delay Automated SMS"),
        choices=ENABLED_DISABLED_CHOICES,
        widget=SelectToggle(choices=ENABLED_DISABLED_CHOICES,
                            attrs={"ko_value": "use_sms_conversation_times"}),
    )
    sms_conversation_times_json = CharField(
        required=False,
        widget=forms.HiddenInput,
    )
    sms_conversation_length = ChoiceField(
        required=False,
        label=ugettext_noop("Conversation Duration"),
        choices=SMS_CONVERSATION_LENGTH_CHOICES,
    )
    survey_traffic_option = ChoiceField(
        required=False,
        label=ugettext_noop("Survey Traffic"),
        choices=(
            (SHOW_ALL, ugettext_noop("Show all survey traffic")),
            (SHOW_INVALID,
             ugettext_noop("Hide all survey traffic except "
                           "invalid responses")),
            (HIDE_ALL, ugettext_noop("Hide all survey traffic")),
        ),
    )
    count_messages_as_read_by_anyone = ChoiceField(
        required=False,
        label=ugettext_noop("A Message is Read..."),
        choices=(
            (ENABLED, ugettext_noop("when it is read by anyone")),
            (DISABLED, ugettext_noop("only for the user that reads it")),
        ),
    )
    use_custom_chat_template = ChoiceField(
        required=False,
        choices=DEFAULT_CUSTOM_CHOICES,
    )
    custom_chat_template = TrimmedCharField(
        required=False,
        label=ugettext_noop("Enter Chat Template Identifier"),
    )

    # Registration settings
    sms_case_registration_enabled = ChoiceField(
        required=False,
        choices=ENABLED_DISABLED_CHOICES,
        label=ugettext_noop("Case Self-Registration"),
    )
    sms_case_registration_type = TrimmedCharField(
        required=False,
        label=ugettext_noop("Default Case Type"),
    )
    sms_case_registration_owner_id = CharField(
        required=False,
        label=ugettext_noop("Default Case Owner"),
        widget=forms.Select(choices=[]),
    )
    sms_case_registration_user_id = CharField(
        required=False,
        label=ugettext_noop("Registration Submitter"),
        widget=forms.Select(choices=[]),
    )
    sms_mobile_worker_registration_enabled = ChoiceField(
        required=False,
        choices=ENABLED_DISABLED_CHOICES,
        label=ugettext_noop("SMS Mobile Worker Registration"),
    )
    registration_welcome_message = ChoiceField(
        choices=WELCOME_RECIPIENT_CHOICES,
        label=ugettext_lazy("Send registration welcome message to"),
    )
    language_fallback = ChoiceField(
        choices=LANGUAGE_FALLBACK_CHOICES,
        label=ugettext_lazy("Backup behavior for missing translations"),
    )

    # Internal settings
    override_daily_outbound_sms_limit = ChoiceField(
        required=False,
        choices=ENABLED_DISABLED_CHOICES,
        label=ugettext_lazy("Override Daily Outbound SMS Limit"),
    )
    custom_daily_outbound_sms_limit = IntegerField(
        required=False,
        label=ugettext_noop("Daily Outbound SMS Limit"),
        min_value=1000,
    )

    @property
    def section_general(self):
        fields = [
            hqcrispy.B3MultiField(
                _("Default SMS Response"),
                crispy.Div(InlineField(
                    "use_default_sms_response",
                    data_bind="value: use_default_sms_response",
                ),
                           css_class='col-sm-4'),
                crispy.Div(InlineField(
                    "default_sms_response",
                    css_class="input-xxlarge",
                    placeholder=_("Enter Default Response"),
                    data_bind="visible: showDefaultSMSResponse",
                ),
                           css_class='col-sm-8'),
                help_bubble_text=_(
                    "Enable this option to provide a "
                    "default response when a user's incoming SMS does not "
                    "answer an open survey or match a known keyword."),
                css_id="default-sms-response-group",
                field_class='col-sm-6 col-md-9 col-lg-9'),
            hqcrispy.FieldWithHelpBubble(
                "use_restricted_sms_times",
                data_bind="value: use_restricted_sms_times",
                help_bubble_text=_(
                    "Use this option to limit the times "
                    "that SMS messages can be sent to users. Messages that "
                    "are sent outside these windows will remained queued "
                    "and will go out as soon as another window opens up."),
            ),
            hqcrispy.B3MultiField(
                "",
                hqcrispy.HiddenFieldWithErrors(
                    "restricted_sms_times_json",
                    data_bind="value: restricted_sms_times_json"),
                crispy.Div(data_bind="template: {"
                           " name: 'ko-template-restricted-sms-times', "
                           " data: $data"
                           "}", ),
                data_bind="visible: showRestrictedSMSTimes",
            ),
            hqcrispy.FieldWithHelpBubble(
                'sms_survey_date_format',
                help_bubble_text=_("Choose the format in which date questions "
                                   "should be answered in SMS surveys."),
            ),
        ]
        return crispy.Fieldset(_("General Settings"), *fields)

    @property
    def section_registration(self):
        fields = [
            hqcrispy.FieldWithHelpBubble(
                "sms_case_registration_enabled",
                help_bubble_text=_(
                    "When this option is enabled, a person "
                    "can send an SMS into the system saying 'join "
                    "[project]', where [project] is your project "
                    "space name, and the system will automatically "
                    "create a case tied to that person's phone number."),
                data_bind="value: sms_case_registration_enabled",
            ),
            crispy.Div(
                hqcrispy.FieldWithHelpBubble(
                    "sms_case_registration_type",
                    placeholder=_("Enter a Case Type"),
                    help_bubble_text=_("Cases that self-register over SMS "
                                       "will be given this case type."),
                ),
                hqcrispy.FieldWithHelpBubble(
                    "sms_case_registration_owner_id",
                    help_bubble_text=_(
                        "Cases that self-register over SMS "
                        "will be owned by this user or user group."),
                ),
                hqcrispy.FieldWithHelpBubble(
                    "sms_case_registration_user_id",
                    help_bubble_text=_(
                        "The form submission for a "
                        "self-registration will belong to this user."),
                ),
                data_bind="visible: showRegistrationOptions",
            ),
            hqcrispy.FieldWithHelpBubble(
                "sms_mobile_worker_registration_enabled",
                help_bubble_text=_(
                    "When this option is enabled, a person "
                    "can send an SMS into the system saying 'join "
                    "[project] worker [username]' (where [project] is your "
                    " project space and [username] is an optional username)"
                    ", and the system will add them as a mobile worker."),
            ),
            hqcrispy.FieldWithHelpBubble(
                'registration_welcome_message',
                help_bubble_text=_(
                    "Choose whether to send an automatic "
                    "welcome message to cases, mobile workers, or both, "
                    "after they self-register. The welcome message can be "
                    "configured in the SMS languages and translations page "
                    "(Messaging -> Languages -> Messaging Translations)."),
            ),
            hqcrispy.FieldWithHelpBubble(
                'language_fallback',
                help_bubble_text=_("""
                    Choose what should happen when a broadcast or alert should be sent to a recipient but no
                    translations exists in the user's preferred language. You may choose not to send a message in
                    that case, or to try one of several backups.<br><br>The first backup uses the broadcast or
                    alert's default language. If that translation is also unavailable, the second backup is text in
                    the project's default SMS language. If that translation is also unavailable, you may choose
                    to use untranslated text, if there is any.
                """),
            ),
        ]

        return crispy.Fieldset(_("Registration Settings"), *fields)

    @property
    def section_chat(self):
        fields = [
            hqcrispy.B3MultiField(
                _("Case Name Display"),
                crispy.Div(InlineField(
                    "use_custom_case_username",
                    data_bind="value: use_custom_case_username",
                ),
                           css_class='col-sm-4'),
                crispy.Div(InlineField(
                    "custom_case_username",
                    css_class="input-large",
                    data_bind="visible: showCustomCaseUsername",
                ),
                           css_class='col-sm-8'),
                help_bubble_text=_(
                    "By default, when chatting with a case, "
                    "the chat window will use the case's \"name\" case "
                    "property when displaying the case's name. To use a "
                    "different case property, specify it here."),
                css_id="custom-case-username-group",
                field_class='col-sm-6 col-md-9 col-lg-9'),
            hqcrispy.B3MultiField(
                _("Message Counter"),
                crispy.Div(InlineField(
                    "use_custom_message_count_threshold",
                    data_bind="value: use_custom_message_count_threshold",
                ),
                           css_class='col-sm-4'),
                crispy.Div(InlineField(
                    "custom_message_count_threshold",
                    css_class="input-large",
                    data_bind="visible: showCustomMessageCountThreshold",
                ),
                           css_class='col-sm-8'),
                help_bubble_text=_(
                    "The chat window can use a counter to keep "
                    "track of how many messages are being sent and received "
                    "and highlight that number after a certain threshold is "
                    "reached. By default, the counter is disabled. To enable "
                    "it, enter the desired threshold here."),
                css_id="custom-message-count-threshold-group",
                field_class='col-sm-6 col-md-9 col-lg-9'),
            hqcrispy.FieldWithHelpBubble(
                "use_sms_conversation_times",
                help_bubble_text=_(
                    "When this option is enabled, the system "
                    "will not send automated SMS to chat recipients when "
                    "those recipients are in the middle of a conversation."),
            ),
            hqcrispy.B3MultiField(
                "",
                hqcrispy.HiddenFieldWithErrors(
                    "sms_conversation_times_json",
                    data_bind="value: sms_conversation_times_json"),
                crispy.Div(data_bind="template: {"
                           " name: 'ko-template-sms-conversation-times', "
                           " data: $data"
                           "}", ),
                data_bind="visible: showSMSConversationTimes",
                label_class='hide',
                field_class='col-md-12 col-lg-10'),
            crispy.Div(
                hqcrispy.FieldWithHelpBubble(
                    "sms_conversation_length",
                    help_bubble_text=_(
                        "The number of minutes to wait "
                        "after receiving an incoming SMS from a chat "
                        "recipient before resuming automated SMS to that "
                        "recipient."),
                ),
                data_bind="visible: showSMSConversationTimes",
            ),
            hqcrispy.FieldWithHelpBubble(
                "survey_traffic_option",
                help_bubble_text=_(
                    "This option allows you to hide a chat "
                    "recipient's survey questions and responses from chat "
                    "windows. There is also the option to show only invalid "
                    "responses to questions in the chat window, which could "
                    "be attempts to converse."),
            ),
            hqcrispy.FieldWithHelpBubble(
                "count_messages_as_read_by_anyone",
                help_bubble_text=_(
                    "The chat window will mark unread "
                    "messages to the user viewing them. Use this option to "
                    "control whether a message counts as being read if it "
                    "is read by anyone, or if it counts as being read only "
                    "to the user who reads it."),
            ),
        ]
        return crispy.Fieldset(_("Chat Settings"), *fields)

    @property
    def section_internal(self):
        return crispy.Fieldset(
            _("Internal Settings (Dimagi Only)"),
            hqcrispy.B3MultiField(
                _("Override Daily Outbound SMS Limit"),
                crispy.Div(InlineField(
                    'override_daily_outbound_sms_limit',
                    data_bind='value: override_daily_outbound_sms_limit',
                ),
                           css_class='col-sm-4'),
                crispy.Div(
                    InlineField('custom_daily_outbound_sms_limit'),
                    data_bind=
                    "visible: override_daily_outbound_sms_limit() === '%s'" %
                    ENABLED,
                    css_class='col-sm-8'),
            ),
            hqcrispy.B3MultiField(
                _("Chat Template"),
                crispy.Div(InlineField(
                    "use_custom_chat_template",
                    data_bind="value: use_custom_chat_template",
                ),
                           css_class='col-sm-4'),
                crispy.Div(InlineField(
                    "custom_chat_template",
                    data_bind="visible: showCustomChatTemplate",
                ),
                           css_class='col-sm-8'),
                help_bubble_text=_("To use a custom template to render the "
                                   "chat window, enter it here."),
                css_id="custom-chat-template-group",
            ),
        )

    @property
    def sections(self):
        result = [
            self.section_general,
            self.section_registration,
            self.section_chat,
        ]

        if self.is_previewer:
            result.append(self.section_internal)

        result.append(
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Save"),
                    type="submit",
                    css_class="btn-primary",
                ), ), )

        return result

    def __init__(self,
                 data=None,
                 domain=None,
                 is_previewer=False,
                 *args,
                 **kwargs):
        self.domain = domain
        self.is_previewer = is_previewer
        super(SettingsForm, self).__init__(data, *args, **kwargs)

        self.helper = HQFormHelper()

        self.helper.layout = crispy.Layout(*self.sections)

        self.restricted_sms_times_widget_context = {
            "template_name":
            "ko-template-restricted-sms-times",
            "explanation_text":
            _("SMS will only be sent when any of the following is true:"),
            "ko_array_name":
            "restricted_sms_times",
            "remove_window_method":
            "$parent.removeRestrictedSMSTime",
            "add_window_method":
            "addRestrictedSMSTime",
        }
        self.sms_conversation_times_widget_context = {
            "template_name":
            "ko-template-sms-conversation-times",
            "explanation_text":
            _("Automated SMS will be suppressed during "
              "chat conversations when any of the following "
              "is true:"),
            "ko_array_name":
            "sms_conversation_times",
            "remove_window_method":
            "$parent.removeSMSConversationTime",
            "add_window_method":
            "addSMSConversationTime",
        }

    @property
    def enable_registration_welcome_sms_for_case(self):
        return (self.cleaned_data.get('registration_welcome_message')
                in (WELCOME_RECIPIENT_CASE, WELCOME_RECIPIENT_ALL))

    @property
    def enable_registration_welcome_sms_for_mobile_worker(self):
        return (self.cleaned_data.get('registration_welcome_message')
                in (WELCOME_RECIPIENT_MOBILE_WORKER, WELCOME_RECIPIENT_ALL))

    @property
    def current_values(self):
        current_values = {}
        for field_name in self.fields.keys():
            value = self[field_name].value()
            if field_name in [
                    "restricted_sms_times_json", "sms_conversation_times_json"
            ]:
                if isinstance(value, str):
                    current_values[field_name] = json.loads(value)
                else:
                    current_values[field_name] = value
            elif field_name in [
                    'sms_case_registration_owner_id',
                    'sms_case_registration_user_id'
            ]:
                if value:
                    obj = self.get_user_group_or_location(value)
                    if isinstance(obj, SQLLocation):
                        current_values[field_name] = {
                            'id': value,
                            'text': _("Organization: {}").format(obj.name)
                        }
                    elif isinstance(obj, Group):
                        current_values[field_name] = {
                            'id': value,
                            'text': _("User Group: {}").format(obj.name)
                        }
                    elif isinstance(obj, CommCareUser):
                        current_values[field_name] = {
                            'id': value,
                            'text': _("User: {}").format(obj.raw_username)
                        }
            else:
                current_values[field_name] = value
        return current_values

    def _clean_dependent_field(self, bool_field, field):
        if self.cleaned_data.get(bool_field):
            value = self.cleaned_data.get(field, None)
            if not value:
                raise ValidationError(_("This field is required."))
            return value
        else:
            return None

    def clean_use_default_sms_response(self):
        return self.cleaned_data.get("use_default_sms_response") == ENABLED

    def clean_default_sms_response(self):
        return self._clean_dependent_field("use_default_sms_response",
                                           "default_sms_response")

    def clean_use_custom_case_username(self):
        return self.cleaned_data.get("use_custom_case_username") == CUSTOM

    def clean_custom_case_username(self):
        return self._clean_dependent_field("use_custom_case_username",
                                           "custom_case_username")

    def clean_use_custom_message_count_threshold(self):
        return (self.cleaned_data.get("use_custom_message_count_threshold") ==
                CUSTOM)

    def clean_custom_message_count_threshold(self):
        value = self._clean_dependent_field(
            "use_custom_message_count_threshold",
            "custom_message_count_threshold")
        if value is not None and value <= 0:
            raise ValidationError(_("Please enter a positive number"))
        return value

    def clean_use_custom_chat_template(self):
        if not self.is_previewer:
            return None
        return self.cleaned_data.get("use_custom_chat_template") == CUSTOM

    def clean_custom_chat_template(self):
        if not self.is_previewer:
            return None
        value = self._clean_dependent_field("use_custom_chat_template",
                                            "custom_chat_template")
        if value is not None and value not in settings.CUSTOM_CHAT_TEMPLATES:
            raise ValidationError(_("Unknown custom template identifier."))
        return value

    def _clean_time_window_json(self, field_name):
        try:
            time_window_json = json.loads(self.cleaned_data.get(field_name))
        except ValueError:
            raise ValidationError(
                _("An error has occurred. Please try again, "
                  "and if the problem persists, please report an issue."))
        result = []
        for window in time_window_json:
            day = window.get("day")
            start_time = window.get("start_time")
            end_time = window.get("end_time")
            time_input_relationship = window.get("time_input_relationship")

            try:
                day = int(day)
                assert day >= -1 and day <= 6
            except (ValueError, AssertionError):
                raise ValidationError(_("Invalid day chosen."))

            if time_input_relationship == TIME_BEFORE:
                end_time = validate_time(end_time)
                result.append(
                    DayTimeWindow(
                        day=day,
                        start_time=None,
                        end_time=end_time,
                    ))
            elif time_input_relationship == TIME_AFTER:
                start_time = validate_time(start_time)
                result.append(
                    DayTimeWindow(
                        day=day,
                        start_time=start_time,
                        end_time=None,
                    ))
            else:
                start_time = validate_time(start_time)
                end_time = validate_time(end_time)
                if start_time >= end_time:
                    raise ValidationError(
                        _("End time must come after start "
                          "time."))
                result.append(
                    DayTimeWindow(
                        day=day,
                        start_time=start_time,
                        end_time=end_time,
                    ))
        return result

    def clean_use_restricted_sms_times(self):
        return self.cleaned_data.get("use_restricted_sms_times") == ENABLED

    def clean_restricted_sms_times_json(self):
        if self.cleaned_data.get("use_restricted_sms_times"):
            return self._clean_time_window_json("restricted_sms_times_json")
        else:
            return []

    def clean_use_sms_conversation_times(self):
        return self.cleaned_data.get("use_sms_conversation_times") == ENABLED

    def clean_sms_conversation_times_json(self):
        if self.cleaned_data.get("use_sms_conversation_times"):
            return self._clean_time_window_json("sms_conversation_times_json")
        else:
            return []

    def clean_count_messages_as_read_by_anyone(self):
        return (self.cleaned_data.get("count_messages_as_read_by_anyone") ==
                ENABLED)

    def clean_sms_case_registration_enabled(self):
        return (
            self.cleaned_data.get("sms_case_registration_enabled") == ENABLED)

    def clean_sms_case_registration_type(self):
        return self._clean_dependent_field("sms_case_registration_enabled",
                                           "sms_case_registration_type")

    def get_user_group_or_location(self, object_id):
        try:
            return SQLLocation.active_objects.get(
                domain=self.domain,
                location_id=object_id,
                location_type__shares_cases=True,
            )
        except SQLLocation.DoesNotExist:
            pass

        try:
            group = Group.get(object_id)
            if group.doc_type == 'Group' and group.domain == self.domain and group.case_sharing:
                return group
            elif group.is_deleted:
                return None
        except ResourceNotFound:
            pass

        return self.get_user(object_id)

    def get_user(self, object_id):
        try:
            user = CommCareUser.get(object_id)
            if user.doc_type == 'CommCareUser' and user.domain == self.domain:
                return user
        except ResourceNotFound:
            pass

        return None

    def clean_sms_case_registration_owner_id(self):
        if not self.cleaned_data.get("sms_case_registration_enabled"):
            return None

        value = self.cleaned_data.get("sms_case_registration_owner_id")
        if not value:
            raise ValidationError(_("This field is required."))

        obj = self.get_user_group_or_location(value)
        if not isinstance(obj, (CommCareUser, Group, SQLLocation)):
            raise ValidationError(_("Please select again"))

        return value

    def clean_sms_case_registration_user_id(self):
        if not self.cleaned_data.get("sms_case_registration_enabled"):
            return None

        value = self.cleaned_data.get("sms_case_registration_user_id")
        if not value:
            raise ValidationError(_("This field is required."))

        obj = self.get_user(value)
        if not isinstance(obj, CommCareUser):
            raise ValidationError(_("Please select again"))

        return value

    def clean_sms_mobile_worker_registration_enabled(self):
        return (self.cleaned_data.get("sms_mobile_worker_registration_enabled")
                == ENABLED)

    def clean_sms_conversation_length(self):
        # Just cast to int, the ChoiceField will validate that it is an integer
        return int(self.cleaned_data.get("sms_conversation_length"))

    def clean_custom_daily_outbound_sms_limit(self):
        if not self.is_previewer:
            return None

        if self.cleaned_data.get(
                'override_daily_outbound_sms_limit') != ENABLED:
            return None

        value = self.cleaned_data.get("custom_daily_outbound_sms_limit")
        if not value:
            raise ValidationError(_("This field is required"))

        return value
示例#4
0
class CommCareUserFilterForm(forms.Form):
    USERNAMES_COLUMN_OPTION = 'usernames'
    COLUMNS_CHOICES = (('all', ugettext_noop('All')),
                       (USERNAMES_COLUMN_OPTION,
                        ugettext_noop('Only Usernames')))
    role_id = forms.ChoiceField(label=ugettext_lazy('Role'),
                                choices=(),
                                required=False)
    search_string = forms.CharField(label=ugettext_lazy('Search by username'),
                                    max_length=30,
                                    required=False)
    location_id = forms.CharField(
        label=ugettext_noop("Location"),
        required=False,
    )
    columns = forms.ChoiceField(
        required=False,
        label=ugettext_noop("Columns"),
        choices=COLUMNS_CHOICES,
        widget=SelectToggle(choices=COLUMNS_CHOICES, apply_bindings=True),
    )

    def __init__(self, *args, **kwargs):
        from corehq.apps.locations.forms import LocationSelectWidget
        self.domain = kwargs.pop('domain')
        super(CommCareUserFilterForm, self).__init__(*args, **kwargs)
        self.fields['location_id'].widget = LocationSelectWidget(self.domain)
        self.fields[
            'location_id'].help_text = ExpandedMobileWorkerFilter.location_search_help

        roles = UserRole.by_domain(self.domain)
        self.fields['role_id'].choices = [('', _('All Roles'))] + [
            (role._id, role.name or _('(No Name)')) for role in roles
        ]

        self.helper = FormHelper()
        self.helper.form_method = 'GET'
        self.helper.form_id = 'user-filters'
        self.helper.form_class = 'form-horizontal'
        self.helper.form_action = reverse('download_commcare_users',
                                          args=[self.domain])

        self.helper.label_class = 'col-sm-3 col-md-2'
        self.helper.field_class = 'col-sm-9 col-md-8 col-lg-6'
        self.helper.form_text_inline = True

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Filter and Download Users"),
                crispy.Field('role_id', css_class="hqwebapp-select2"),
                crispy.Field('search_string'),
                crispy.Field('location_id'),
                crispy.Field('columns'),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Download All Users"),
                    type="submit",
                    css_class="btn btn-primary submit_button",
                )),
        )

    def clean_role_id(self):
        role_id = self.cleaned_data['role_id']
        if not role_id:
            return None
        if not UserRole.get(role_id).domain == self.domain:
            raise forms.ValidationError(_("Invalid Role"))
        return role_id

    def clean_search_string(self):
        search_string = self.cleaned_data['search_string']
        if "*" in search_string or "?" in search_string:
            raise forms.ValidationError(_("* and ? are not allowed"))
        return search_string
示例#5
0
class CaseRuleCriteriaForm(forms.Form):
    # Prefix to avoid name collisions; this means all input
    # names in the HTML are prefixed with "criteria-"
    prefix = "criteria"

    case_type = forms.ChoiceField(
        label=gettext_lazy("Case Type"),
        required=True,
    )
    criteria_operator = forms.ChoiceField(
        label=gettext_lazy("Run when"),
        required=False,
        initial='ALL',
        choices=AutomaticUpdateRule.CriteriaOperator.choices,
        widget=SelectToggle(
            choices=AutomaticUpdateRule.CriteriaOperator.choices,
            attrs={"ko_value": "criteriaOperator"}),
    )

    filter_on_server_modified = forms.CharField(required=False,
                                                initial='false')
    server_modified_boundary = forms.CharField(required=False, initial='')
    custom_match_definitions = forms.CharField(required=False, initial='[]')
    property_match_definitions = forms.CharField(required=False, initial='[]')
    filter_on_closed_parent = forms.CharField(required=False, initial='false')

    @property
    def current_values(self):
        return {
            'filter_on_server_modified':
            self['filter_on_server_modified'].value(),
            'server_modified_boundary':
            self['server_modified_boundary'].value(),
            'custom_match_definitions':
            json.loads(self['custom_match_definitions'].value()),
            'property_match_definitions':
            json.loads(self['property_match_definitions'].value()),
            'filter_on_closed_parent':
            self['filter_on_closed_parent'].value(),
            'case_type':
            self['case_type'].value(),
            'criteria_operator':
            self['criteria_operator'].value(),
        }

    @property
    def constants(self):
        return {
            'MATCH_DAYS_BEFORE': MatchPropertyDefinition.MATCH_DAYS_BEFORE,
            'MATCH_DAYS_AFTER': MatchPropertyDefinition.MATCH_DAYS_AFTER,
            'MATCH_EQUAL': MatchPropertyDefinition.MATCH_EQUAL,
            'MATCH_NOT_EQUAL': MatchPropertyDefinition.MATCH_NOT_EQUAL,
            'MATCH_HAS_VALUE': MatchPropertyDefinition.MATCH_HAS_VALUE,
            'MATCH_HAS_NO_VALUE': MatchPropertyDefinition.MATCH_HAS_NO_VALUE,
            'MATCH_REGEX': MatchPropertyDefinition.MATCH_REGEX,
        }

    def compute_initial(self, domain, rule):
        initial = {
            'case_type':
            rule.case_type,
            'criteria_operator':
            rule.criteria_operator,
            'filter_on_server_modified':
            'true' if rule.filter_on_server_modified else 'false',
            'server_modified_boundary':
            rule.server_modified_boundary,
        }

        custom_match_definitions = []
        property_match_definitions = []

        for criteria in rule.memoized_criteria:
            definition = criteria.definition
            if isinstance(definition, MatchPropertyDefinition):
                property_match_definitions.append({
                    'property_name':
                    definition.property_name,
                    'property_value':
                    definition.property_value,
                    'match_type':
                    definition.match_type,
                })
            elif isinstance(definition, CustomMatchDefinition):
                custom_match_definitions.append({
                    'name': definition.name,
                })
            elif isinstance(definition, ClosedParentDefinition):
                initial['filter_on_closed_parent'] = 'true'

        initial['custom_match_definitions'] = json.dumps(
            custom_match_definitions)
        initial['property_match_definitions'] = json.dumps(
            property_match_definitions)
        return initial

    @property
    def show_fieldset_title(self):
        return True

    @property
    def fieldset_help_text(self):
        return _(
            "The Actions will be performed for all open cases that match all filter criteria below."
        )

    @property
    def allow_parent_case_references(self):
        return True

    @property
    def allow_case_modified_filter(self):
        return True

    @property
    def allow_case_property_filter(self):
        return True

    @property
    def allow_date_case_property_filter(self):
        return True

    @property
    def allow_regex_case_property_match(self):
        # The framework allows for this, it's just historically only
        # been an option for messaging conditonal alert rules and not
        # case update rules. So for now the option is just hidden in
        # the case update rule UI.
        return False

    def __init__(self, domain, *args, **kwargs):
        if 'initial' in kwargs:
            raise ValueError(_("Initial values are set by the form."))

        self.is_system_admin = kwargs.pop('is_system_admin', False)

        self.initial_rule = kwargs.pop('rule', None)
        if self.initial_rule:
            kwargs['initial'] = self.compute_initial(domain, self.initial_rule)

        super(CaseRuleCriteriaForm, self).__init__(*args, **kwargs)

        self.domain = domain
        self.set_case_type_choices(self.initial.get('case_type'))
        self.fields[
            'criteria_operator'].choices = AutomaticUpdateRule.CriteriaOperator.choices

        self.helper = HQFormHelper()
        self.helper.form_tag = False
        self.helper.layout = Layout(
            Fieldset(
                _("Case Filters") if self.show_fieldset_title else "",
                HTML(
                    '<p class="help-block alert alert-info"><i class="fa fa-info-circle"></i> %s</p>'
                    % self.fieldset_help_text),
                hidden_bound_field('filter_on_server_modified',
                                   'filterOnServerModified'),
                hidden_bound_field('server_modified_boundary',
                                   'serverModifiedBoundary'),
                hidden_bound_field('custom_match_definitions',
                                   'customMatchDefinitions'),
                hidden_bound_field('property_match_definitions',
                                   'propertyMatchDefinitions'),
                hidden_bound_field('filter_on_closed_parent',
                                   'filterOnClosedParent'),
                Div(data_bind="template: {name: 'case-filters'}"),
                css_id="rule-criteria-panel",
            ), )

        self.form_beginning_helper = HQFormHelper()
        self.form_beginning_helper.form_tag = False
        self.form_beginning_helper.layout = Layout(
            Fieldset(
                _("Rule Criteria"),
                Field('case_type',
                      data_bind="value: caseType",
                      css_class="hqwebapp-select2"),
                Field('criteria_operator'),
            ))

        self.custom_filters = settings.AVAILABLE_CUSTOM_RULE_CRITERIA.keys()

    @property
    @memoized
    def requires_system_admin_to_edit(self):
        if 'custom_match_definitions' not in self.initial:
            return False

        custom_criteria = json.loads(self.initial['custom_match_definitions'])
        return len(custom_criteria) > 0

    @property
    @memoized
    def requires_system_admin_to_save(self):
        return len(self.cleaned_data['custom_match_definitions']) > 0

    def _json_fail_hard(self):
        raise ValueError(_("Invalid JSON object given"))

    def set_case_type_choices(self, initial):
        case_types = [''] + list(get_case_types_for_domain(self.domain))
        if initial and initial not in case_types:
            # Include the deleted case type in the list of choices so that
            # we always allow proper display and edit of rules
            case_types.append(initial)
        case_types.sort()
        self.fields['case_type'].choices = ((case_type, case_type)
                                            for case_type in case_types)

    def clean_filter_on_server_modified(self):
        return true_or_false(
            self.cleaned_data.get('filter_on_server_modified'))

    def clean_server_modified_boundary(self):
        # Be explicit about this check to prevent any accidents in the future
        if self.cleaned_data['filter_on_server_modified'] is False:
            return None

        value = self.cleaned_data.get('server_modified_boundary')
        return validate_non_negative_days(value)

    def clean_custom_match_definitions(self):
        value = self.cleaned_data.get('custom_match_definitions')
        try:
            value = json.loads(value)
        except (TypeError, ValueError):
            self._json_fail_hard()

        if not isinstance(value, list):
            self._json_fail_hard()

        result = []

        for obj in value:
            if not isinstance(obj, dict):
                self._json_fail_hard()

            if 'name' not in obj:
                self._json_fail_hard()

            name = obj['name'].strip()

            result.append({'name': name})

        return result

    def clean_property_match_definitions(self):
        value = self.cleaned_data.get('property_match_definitions')
        try:
            value = json.loads(value)
        except (TypeError, ValueError):
            self._json_fail_hard()

        if not isinstance(value, list):
            self._json_fail_hard()

        result = []

        for obj in value:
            if not isinstance(obj, dict):
                self._json_fail_hard()

            if ('property_name' not in obj or 'property_value' not in obj
                    or 'match_type' not in obj):
                self._json_fail_hard()

            property_name = validate_case_property_name(
                obj['property_name'],
                allow_parent_case_references=self.allow_parent_case_references)
            match_type = obj['match_type']
            if match_type not in MatchPropertyDefinition.MATCH_CHOICES:
                self._json_fail_hard()

            if match_type in (
                    MatchPropertyDefinition.MATCH_HAS_VALUE,
                    MatchPropertyDefinition.MATCH_HAS_NO_VALUE,
            ):
                result.append({
                    'property_name': property_name,
                    'property_value': None,
                    'match_type': match_type,
                })
            elif match_type in (
                    MatchPropertyDefinition.MATCH_EQUAL,
                    MatchPropertyDefinition.MATCH_NOT_EQUAL,
            ):
                property_value = validate_case_property_value(
                    obj['property_value'])
                result.append({
                    'property_name': property_name,
                    'property_value': property_value,
                    'match_type': match_type,
                })
            elif match_type in (
                    MatchPropertyDefinition.MATCH_DAYS_BEFORE,
                    MatchPropertyDefinition.MATCH_DAYS_AFTER,
            ):
                property_value = obj['property_value']
                try:
                    property_value = int(property_value)
                except (TypeError, ValueError):
                    raise ValidationError(_("Please enter a number of days"))

                result.append({
                    'property_name': property_name,
                    'property_value': str(property_value),
                    'match_type': match_type,
                })
            elif match_type == MatchPropertyDefinition.MATCH_REGEX:
                property_value = obj['property_value']

                if not property_value:
                    raise ValidationError(
                        _("Please enter a valid regular expression to match"))

                try:
                    re.compile(property_value)
                except (re.error, ValueError, TypeError):
                    raise ValidationError(
                        _("Please enter a valid regular expression to match"))

                result.append({
                    'property_name': property_name,
                    'property_value': property_value,
                    'match_type': match_type,
                })

        return result

    def clean_filter_on_closed_parent(self):
        return true_or_false(self.cleaned_data.get('filter_on_closed_parent'))

    def save_criteria(self, rule):
        with transaction.atomic():
            rule.case_type = self.cleaned_data['case_type']
            rule.criteria_operator = self.cleaned_data['criteria_operator']
            rule.filter_on_server_modified = self.cleaned_data[
                'filter_on_server_modified']
            rule.server_modified_boundary = self.cleaned_data[
                'server_modified_boundary']
            rule.save()

            rule.delete_criteria()

            for item in self.cleaned_data['property_match_definitions']:
                definition = MatchPropertyDefinition.objects.create(
                    property_name=item['property_name'],
                    property_value=item['property_value'],
                    match_type=item['match_type'],
                )

                criteria = CaseRuleCriteria(rule=rule)
                criteria.definition = definition
                criteria.save()

            for item in self.cleaned_data['custom_match_definitions']:
                definition = CustomMatchDefinition.objects.create(
                    name=item['name'], )

                criteria = CaseRuleCriteria(rule=rule)
                criteria.definition = definition
                criteria.save()

            if self.cleaned_data['filter_on_closed_parent']:
                definition = ClosedParentDefinition.objects.create()

                criteria = CaseRuleCriteria(rule=rule)
                criteria.definition = definition
                criteria.save()
示例#6
0
class LocationFilterForm(forms.Form):
    ACTIVE = 'active'
    ARCHIVED = 'archived'
    SHOW_ALL = 'show_all'

    LOCATION_ACTIVE_STATUS = ((SHOW_ALL, gettext_lazy('Show All')),
                              (ACTIVE, gettext_lazy('Only Active')),
                              (ARCHIVED, gettext_lazy('Only Archived')))

    location_id = forms.CharField(
        label=gettext_noop("Location"),
        required=False,
    )
    selected_location_only = forms.BooleanField(
        required=False,
        label=_('Only include selected location'),
        initial=False,
    )
    location_status_active = forms.ChoiceField(
        label=_('Active / Archived'),
        choices=LOCATION_ACTIVE_STATUS,
        required=False,
        widget=SelectToggle(choices=LOCATION_ACTIVE_STATUS,
                            attrs={"ko_value": "location_status_active"}),
    )

    def __init__(self, *args, **kwargs):
        self.domain = kwargs.pop('domain')
        self.user = kwargs.pop('user')
        super().__init__(*args, **kwargs)
        self.fields['location_id'].widget = LocationSelectWidget(
            self.domain,
            id='id_location_id',
            placeholder=_("All Locations"),
            attrs={'data-bind': 'value: location_id'},
        )
        self.fields[
            'location_id'].widget.query_url = "{url}?show_all=true".format(
                url=self.fields['location_id'].widget.query_url)

        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'GET'
        self.helper.form_id = 'locations-filters'
        self.helper.form_action = reverse('location_export',
                                          args=[self.domain])

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Filter and Download Locations"),
                crispy.Field('location_id', ),
                crispy.Div(
                    crispy.Field('selected_location_only',
                                 data_bind='checked: selected_location_only'),
                    data_bind="slideVisible: location_id",
                ),
                crispy.Field('location_status_active', ),
            ),
            hqcrispy.FormActions(
                StrictButton(
                    _("Download Locations"),
                    type="submit",
                    css_class="btn btn-primary",
                    data_bind="html: buttonHTML",
                ), ),
        )

    def clean_location_id(self):
        if self.cleaned_data['location_id'] == '':
            return None
        return self.cleaned_data['location_id']

    def clean_location_status_active(self):
        location_active_status = self.cleaned_data['location_status_active']

        if location_active_status == self.ACTIVE:
            return True
        if location_active_status == self.ARCHIVED:
            return False
        return None

    def is_valid(self):
        if not super().is_valid():
            return False
        location_id = self.cleaned_data.get('location_id')
        if location_id is None:
            return True
        return user_can_access_location_id(self.domain, self.user, location_id)

    def get_filters(self):
        """
        This function translates some form inputs to their relevant SQLLocation attributes
        """
        location_id = self.cleaned_data.get('location_id')
        if (location_id and user_can_access_location_id(
                self.domain, self.user, location_id)):
            location_ids = [location_id]
        else:
            location_ids = []

        filters = {
            'location_ids':
            location_ids,
            'selected_location_only':
            self.cleaned_data.get('selected_location_only', False)
        }
        location_status_active = self.cleaned_data.get(
            'location_status_active', None)

        if location_status_active is not None:
            filters['is_archived'] = (not location_status_active)

        return filters
示例#7
0
class CommCareUserFilterForm(forms.Form):
    USERNAMES_COLUMN_OPTION = 'usernames'
    COLUMNS_CHOICES = (
        ('all', ugettext_noop('All')),
        (USERNAMES_COLUMN_OPTION, ugettext_noop('Only Usernames'))
    )
    role_id = forms.ChoiceField(label=ugettext_lazy('Role'), choices=(), required=False)
    search_string = forms.CharField(
        label=ugettext_lazy('Search by username'),
        max_length=30,
        required=False
    )
    location_id = forms.CharField(
        label=ugettext_noop("Location"),
        required=False,
    )
    columns = forms.ChoiceField(
        required=False,
        label=ugettext_noop("Columns"),
        choices=COLUMNS_CHOICES,
        widget=SelectToggle(choices=COLUMNS_CHOICES, apply_bindings=True),
    )
    domains = forms.MultipleChoiceField(
        required=False,
        label=_('Project Spaces'),
        widget=forms.SelectMultiple(attrs={'class': 'hqwebapp-select2'}),
        help_text=_('Add project spaces containing the desired mobile workers'),
    )

    def __init__(self, *args, **kwargs):
        from corehq.apps.locations.forms import LocationSelectWidget
        from corehq.apps.users.views import get_editable_role_choices
        self.domain = kwargs.pop('domain')
        self.couch_user = kwargs.pop('couch_user')
        super(CommCareUserFilterForm, self).__init__(*args, **kwargs)
        self.fields['location_id'].widget = LocationSelectWidget(self.domain)
        self.fields['location_id'].help_text = ExpandedMobileWorkerFilter.location_search_help

        if is_icds_cas_project(self.domain) and not self.couch_user.is_domain_admin(self.domain):
            roles = get_editable_role_choices(self.domain, self.couch_user, allow_admin_role=True,
                                              use_qualified_id=False)
            self.fields['role_id'].choices = roles
        else:
            roles = UserRole.by_domain(self.domain)
            self.fields['role_id'].choices = [('', _('All Roles'))] + [
                (role._id, role.name or _('(No Name)')) for role in roles]

        self.fields['domains'].choices = [(self.domain, self.domain)]
        if len(DomainPermissionsMirror.mirror_domains(self.domain)) > 0:
            self.fields['domains'].choices = [('all_project_spaces', _('All Project Spaces'))] + \
                                             [(self.domain, self.domain)] + \
                                             [(domain, domain) for domain in
                                              DomainPermissionsMirror.mirror_domains(self.domain)]
        self.helper = FormHelper()
        self.helper.form_method = 'GET'
        self.helper.form_id = 'user-filters'
        self.helper.form_class = 'form-horizontal'
        self.helper.form_action = reverse('download_commcare_users', args=[self.domain])

        self.helper.label_class = 'col-sm-3 col-md-2'
        self.helper.field_class = 'col-sm-9 col-md-8 col-lg-6'
        self.helper.form_text_inline = True

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Filter and Download Users"),
                crispy.Field('role_id', css_class="hqwebapp-select2"),
                crispy.Field('search_string'),
                crispy.Field('location_id'),
                crispy.Field('columns'),
                crispy.Field('domains'),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Download All Users"),
                    type="submit",
                    css_class="btn btn-primary submit_button",
                )
            ),
        )

    def clean_role_id(self):
        role_id = self.cleaned_data['role_id']
        restricted_role_access = (
            is_icds_cas_project(self.domain)
            and not self.couch_user.is_domain_admin(self.domain)
        )
        if not role_id:
            if restricted_role_access:
                raise forms.ValidationError(_("Please select a role"))
            else:
                return None

        role = UserRole.get(role_id)
        if not role.domain == self.domain:
            raise forms.ValidationError(_("Invalid Role"))
        if restricted_role_access:
            try:
                user_role_id = self.couch_user.get_role(self.domain).get_id
            except DomainMembershipError:
                user_role_id = None
            if not role.accessible_by_non_admin_role(user_role_id):
                raise forms.ValidationError(_("Role Access Denied"))
        return role_id

    def clean_search_string(self):
        search_string = self.cleaned_data['search_string']
        if "*" in search_string or "?" in search_string:
            raise forms.ValidationError(_("* and ? are not allowed"))
        return search_string

    def clean_domains(self):
        if 'domains' in self.data:
            domains = self.data.getlist('domains')
        else:
            domains = self.data.getlist('domains[]', [self.domain])

        if 'all_project_spaces' in domains:
            domains = DomainPermissionsMirror.mirror_domains(self.domain)
            domains += [self.domain]
        return domains