codeforamerica/intake

View on GitHub
formation/validators.py

Summary

Maintainability
A
0 mins
Test Coverage
from datetime import date
from formation.exceptions import NoChoicesGivenError
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

from intake.exceptions import MailgunAPIError
from intake.services.mailgun_api_service import \
    validate_email_with_mailgun
from project.alerts import send_email_to_admins
from project.jinja2 import oxford_comma
from intake.constants import CONTACT_PREFERENCE_CHECKS
from project.services.logging_service import format_and_log


class ValidChoiceValidator:
    """Checks that the input data is a valid choice.
    Calls `.add_error()` on the context with a message
    reporting the invalid input.
    """

    not_found_error = _("{} is not a valid choice.")

    def set_context(self, field):
        if not hasattr(field, 'choices'):
            raise NoChoicesGivenError(str(
                "`{}` doesn't have a `choices` attribute.".format(
                    field
                )))
        self.field = field
        self.possible_choices = {
            key: value
            for key, value in field.choices
        }

    def __call__(self, data):
        if data or self.field.required:
            if data not in self.possible_choices:
                self.field.add_error(
                    self.not_found_error.format(data))


class MultipleValidChoiceValidator(ValidChoiceValidator):
    """Checks that each item in the input data is
    a valid choice. Calls `.add_error()` on the context
    with a message reporting which items were invalid.
    """

    multiple_not_found_error = _(
        "{} are not valid choices.")

    def format_error_message(self, missing_values):
        things = ["{}".format(val) for val in missing_values]
        if len(things) == 1:
            fragment = things[0]
            template = self.not_found_error
        else:
            fragment = oxford_comma(things)
            template = self.multiple_not_found_error
        return template.format(fragment)

    def __call__(self, data):
        missing_values = []
        for choice in data:
            if choice not in self.possible_choices:
                missing_values.append(choice)
        if missing_values:
            self.field.add_error(
                self.format_error_message(missing_values))


class CheckEmptyFieldValidator:

    def set_context(self, form):
        self.context = form

    def field_is_empty(self, field_name):
        field = getattr(self.context, field_name)
        return field.is_empty()


class GavePreferredContactMethods(CheckEmptyFieldValidator):
    """Implements the validator protocol of Django REST Framework
        - needs to be callable
        - receives the parent form through the `set_context(form)` method
        - if it finds errors, it should raise a ValidationError to return them
    """
    message_template = _(
        "'{medium}' is set as the preferred contact method, but "
        "you didn't enter {datum}.")

    def message(self, preference):
        attributes, medium, datum = CONTACT_PREFERENCE_CHECKS[preference]
        return self.message_template.format(medium=medium.title(), datum=datum)

    def __call__(self, parsed_data):
        errors = {}
        for key in parsed_data.get('contact_preferences', []):
            attribute_name, medium, datum = CONTACT_PREFERENCE_CHECKS[key]
            if self.field_is_empty(attribute_name):
                field_errors = errors.get(attribute_name, [])
                field_errors.append(self.message(key))
                errors[attribute_name] = field_errors
        if errors:
            raise ValidationError(errors)


class AtLeastEmailOrPhoneValidator(CheckEmptyFieldValidator):

    message = _(
        "We need at least one way to contact you. Please enter a phone number "
        "or an email.")

    field_keys = ['email', 'phone_number']

    def __call__(self, parsed_data):
        if self.context.prefix:
            self.field_keys = [
                self.context.prefix + key for key in self.field_keys]
        errors = {}
        all_fields_empty = all([
            self.field_is_empty(key) for key in self.field_keys])
        if all_fields_empty:
            for key in self.field_keys:
                field_errors = errors.get(key, [])
                field_errors.append(self.message)
                errors[key] = field_errors
        if errors:
            raise ValidationError(errors)


at_least_email_or_phone = AtLeastEmailOrPhoneValidator()
is_a_valid_choice = ValidChoiceValidator()
are_valid_choices = MultipleValidChoiceValidator()
gave_preferred_contact_methods = GavePreferredContactMethods()


def mailgun_email_validator(value):
    message = _('The email address you entered does not appear to exist.')
    suggestion_template = ' Did you mean {}?'
    try:
        email_is_good, suggestion = validate_email_with_mailgun(value)
        if not email_is_good:
            if suggestion:
                message += suggestion_template.format(suggestion)
            raise ValidationError(message)
    except MailgunAPIError as err:
        send_email_to_admins(
            subject="Unexpected MailgunAPIError",
            message="{}".format(err))
        format_and_log(
            'mailgun_api_error', level='error', exception=str(err))


def is_a_valid_date(dob_dict):
    try:
        date(**dob_dict)
    except (ValueError, TypeError) as err:
        error_message = str(err)
        if 'day is out of range for month' in error_message:
            message = str(
                '{} is not a day in that month. '
                'Please enter a valid date'.format(dob_dict['day']))
        else:
            message = 'Please enter a valid date'
        raise ValidationError(message)