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
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
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
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
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()
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
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