Пример #1
0
def parse_sms_text(xform, identity, text):

    json_survey = json.loads(xform.json)

    separator = json_survey.get('sms_separator', DEFAULT_SEPARATOR) \
        or DEFAULT_SEPARATOR

    try:
        allow_media = bool(json_survey.get('sms_allow_media', False))
    except:
        raise
        allow_media = False

    xlsf_date_fmt = json_survey.get('sms_date_format', DEFAULT_DATE_FORMAT) \
        or DEFAULT_DATE_FORMAT
    xlsf_datetime_fmt = json_survey.get('sms_date_format',
                                        DEFAULT_DATETIME_FORMAT) \
        or DEFAULT_DATETIME_FORMAT

    # extract SMS data into indexed groups of values
    groups = {}
    for group in text.split(separator)[1:]:
        group_id, group_text = [s.strip() for s in group.split(None, 1)]
        groups.update({group_id: [s.strip() for s in group_text.split(None)]})

    def cast_sms_value(value, question, medias=[]):
        ''' Check data type of value and return cleaned version '''

        xlsf_type = question.get('type')
        xlsf_name = question.get('name')
        xlsf_choices = question.get('children')
        xlsf_required = bool(
            question.get('bind', {}).get('required', '').lower() in ('yes',
                                                                     'true'))

        # we don't handle constraint for now as it's a little complex and
        # unsafe.
        # xlsf_constraint=question.get('constraint')

        if xlsf_required and not len(value):
            raise SMSCastingError(_(u"Required field missing"), xlsf_name)

        def safe_wrap(func):
            try:
                return func()
            except Exception as e:
                raise SMSCastingError(
                    _(u"%(error)s") % {'error': e}, xlsf_name)

        def media_value(value, medias):
            ''' handle media values

                extract name and base64 data.
                fills the media holder with (name, data) tuple '''
            try:
                filename, b64content = value.split(';', 1)
                medias.append((filename, base64.b64decode(b64content)))
                return filename
            except Exception as e:
                raise SMSCastingError(
                    _(u"Media file format incorrect. %(except)s") %
                    {'except': repr(e)}, xlsf_name)

        if xlsf_type == 'text':
            return safe_wrap(lambda: unicode(value))
        elif xlsf_type == 'integer':
            return safe_wrap(lambda: int(value))
        elif xlsf_type == 'decimal':
            return safe_wrap(lambda: float(value))
        elif xlsf_type == 'select one':
            for choice in xlsf_choices:
                if choice.get('sms_option') == value:
                    return choice.get('name')
            raise SMSCastingError(
                _(u"No matching choice "
                  u"for '%(input)s'") % {'input': value}, xlsf_name)
        elif xlsf_type == 'select all that apply':
            values = [s.strip() for s in value.split()]
            ret_values = []
            for indiv_value in values:
                for choice in xlsf_choices:
                    if choice.get('sms_option') == indiv_value:
                        ret_values.append(choice.get('name'))
            return u" ".join(ret_values)
        elif xlsf_type == 'geopoint':
            err_msg = _(u"Incorrect geopoint coordinates.")
            geodata = [s.strip() for s in value.split()]
            if len(geodata) < 2 and len(geodata) > 4:
                raise SMSCastingError(err_msg, xlsf_name)
            try:
                # check that latitude and longitude are floats
                lat, lon = [float(v) for v in geodata[:2]]
                # and within sphere boundaries
                if lat < -90 or lat > 90 or lon < -180 and lon > 180:
                    raise SMSCastingError(err_msg, xlsf_name)
                if len(geodata) == 4:
                    # check that altitude and accuracy are integers
                    [int(v) for v in geodata[2:4]]
                elif len(geodata) == 3:
                    # check that altitude is integer
                    int(geodata[2])
            except Exception as e:
                raise SMSCastingError(e.message, xlsf_name)
            return " ".join(geodata)

        elif xlsf_type in MEDIA_TYPES:
            # media content (image, video, audio) must be formatted as:
            # file_name;base64 encodeed content.
            # Example: hello.jpg;dGhpcyBpcyBteSBwaWN0dXJlIQ==
            return media_value(value, medias)
        elif xlsf_type == 'barcode':
            return safe_wrap(lambda: unicode(value))
        elif xlsf_type == 'date':
            return safe_wrap(
                lambda: datetime.strptime(value, xlsf_date_fmt).date())
        elif xlsf_type == 'datetime':
            return safe_wrap(
                lambda: datetime.strptime(value, xlsf_datetime_fmt))
        elif xlsf_type == 'note':
            return safe_wrap(lambda: '')
        raise SMSCastingError(
            _(u"Unsuported column '%(type)s'") % {'type': xlsf_type},
            xlsf_name)

    def get_meta_value(xlsf_type, identity):
        ''' XLSForm Meta field value '''
        if xlsf_type in ('deviceid', 'subscriberid', 'imei'):
            return NA_VALUE
        elif xlsf_type in ('start', 'end'):
            return datetime.now().isoformat()
        elif xlsf_type == 'today':
            return date.today().isoformat()
        elif xlsf_type == 'phonenumber':
            return identity
        return NA_VALUE

    # holder for all properly formated answers
    survey_answers = {}
    # list of (name, data) tuples for media contents
    medias = []
    # keep track of required questions
    notes = []

    # loop on all XLSForm questions
    for expected_group in json_survey.get('children', [{}]):
        if not expected_group.get('type') == 'group':
            # non-grouped questions are not valid for SMS
            continue

        # retrieve part of SMS text for this group
        group_id = expected_group.get('sms_field')
        answers = groups.get(group_id)
        if not group_id or (not answers and not group_id.startswith('meta')):
            # group is not meant to be filled by SMS
            # or hasn't been filled
            continue

        # Add a holder for this group's answers data
        survey_answers.update({expected_group.get('name'): {}})

        # retrieve question definition for each answer
        egroups = expected_group.get('children', [{}])

        # number of intermediate, omited questions (medias)
        step_back = 0
        for idx, question in enumerate(egroups):

            real_value = None

            question_type = question.get('type')
            if question_type in ('calculate'):
                # 'calculate' question are not implemented.
                # 'note' ones are just meant to be displayed on device
                continue

            if question_type == 'note':
                if not question.get('constraint', ''):
                    notes.append(question.get('label'))
                continue

            if not allow_media and question_type in MEDIA_TYPES:
                # if medias for SMS has not been explicitly allowed
                # they are considered excluded.
                step_back += 1
                continue

            # pop the number of skipped questions
            # so that out index is valid even if the form
            # contain medias questions (and medias are disabled)
            sidx = idx - step_back

            if question_type in META_FIELDS:
                # some question are not to be fed by users
                real_value = get_meta_value(xlsf_type=question_type,
                                            identity=identity)
            else:
                # actual SMS-sent answer.
                # Only last answer/question of each group is allowed
                # to have multiple spaces
                if is_last(idx, egroups):
                    answer = u" ".join(answers[idx:])
                else:
                    answer = answers[sidx]

            if real_value is None:
                # retrieve actual value and fail if it doesn't meet reqs.
                real_value = cast_sms_value(answer,
                                            question=question,
                                            medias=medias)

            # set value to its question name
            survey_answers[expected_group.get('name')] \
                .update({question.get('name'): real_value})

    return survey_answers, medias, notes
Пример #2
0
def parse_sms_text(xform, identity, text):

    json_survey = json.loads(xform.json)

    separator = json_survey.get('sms_separator', DEFAULT_SEPARATOR) \
        or DEFAULT_SEPARATOR

    allow_media = bool(json_survey.get('sms_allow_media', False))

    xlsf_date_fmt = json_survey.get('sms_date_format', DEFAULT_DATE_FORMAT) \
        or DEFAULT_DATE_FORMAT
    xlsf_datetime_fmt = json_survey.get('sms_date_format',
                                        DEFAULT_DATETIME_FORMAT) \
        or DEFAULT_DATETIME_FORMAT

    # extract SMS data into indexed groups of values
    groups = {}
    for group in text.split(separator)[1:]:
        group_id, group_text = [s.strip() for s in group.split(None, 1)]
        groups.update({group_id: [s.strip() for s in group_text.split(None)]})

    def cast_sms_value(value, question, medias=[]):
        ''' Check data type of value and return cleaned version '''

        xlsf_type = question.get('type')
        xlsf_name = question.get('name')
        xlsf_choices = question.get('children')
        xlsf_required = bool(question.get('bind', {})
                             .get('required', '').lower() in ('yes', 'true'))

        # we don't handle constraint for now as it's a little complex and
        # unsafe.
        # xlsf_constraint=question.get('constraint')

        if xlsf_required and not len(value):
            raise SMSCastingError(_(u"Required field missing"), xlsf_name)

        def safe_wrap(func):
            try:
                return func()
            except Exception as e:
                raise SMSCastingError(_(u"%(error)s") % {'error': e},
                                      xlsf_name)

        def media_value(value, medias):
            ''' handle media values

                extract name and base64 data.
                fills the media holder with (name, data) tuple '''
            try:
                filename, b64content = value.split(';', 1)
                medias.append((filename,
                               base64.b64decode(b64content)))
                return filename
            except Exception as e:
                raise SMSCastingError(_(u"Media file format "
                                      u"incorrect. %(except)r")
                                      % {'except': e}, xlsf_name)

        if xlsf_type == 'text':
            return safe_wrap(lambda: str(value))
        elif xlsf_type == 'integer':
            return safe_wrap(lambda: int(value))
        elif xlsf_type == 'decimal':
            return safe_wrap(lambda: float(value))
        elif xlsf_type == 'select one':
            for choice in xlsf_choices:
                if choice.get('sms_option') == value:
                    return choice.get('name')
            raise SMSCastingError(_(u"No matching choice "
                                    u"for '%(input)s'")
                                  % {'input': value},
                                  xlsf_name)
        elif xlsf_type == 'select all that apply':
            values = [s.strip() for s in value.split()]
            ret_values = []
            for indiv_value in values:
                for choice in xlsf_choices:
                    if choice.get('sms_option') == indiv_value:
                        ret_values.append(choice.get('name'))
            return u" ".join(ret_values)
        elif xlsf_type == 'geopoint':
            err_msg = _(u"Incorrect geopoint coordinates.")
            geodata = [s.strip() for s in value.split()]
            if len(geodata) < 2 and len(geodata) > 4:
                raise SMSCastingError(err_msg, xlsf_name)
            try:
                # check that latitude and longitude are floats
                lat, lon = [float(v) for v in geodata[:2]]
                # and within sphere boundaries
                if lat < -90 or lat > 90 or lon < -180 and lon > 180:
                    raise SMSCastingError(err_msg, xlsf_name)
                if len(geodata) == 4:
                    # check that altitude and accuracy are integers
                    [int(v) for v in geodata[2:4]]
                elif len(geodata) == 3:
                    # check that altitude is integer
                    int(geodata[2])
            except Exception as e:
                raise SMSCastingError(e, xlsf_name)
            return " ".join(geodata)

        elif xlsf_type in MEDIA_TYPES:
            # media content (image, video, audio) must be formatted as:
            # file_name;base64 encodeed content.
            # Example: hello.jpg;dGhpcyBpcyBteSBwaWN0dXJlIQ==
            return media_value(value, medias)
        elif xlsf_type == 'barcode':
            return safe_wrap(lambda: text(value))
        elif xlsf_type == 'date':
            return safe_wrap(lambda: datetime.strptime(value,
                                                       xlsf_date_fmt).date())
        elif xlsf_type == 'datetime':
            return safe_wrap(lambda: datetime.strptime(value,
                                                       xlsf_datetime_fmt))
        elif xlsf_type == 'note':
            return safe_wrap(lambda: '')
        raise SMSCastingError(_(u"Unsuported column '%(type)s'")
                              % {'type': xlsf_type}, xlsf_name)

    def get_meta_value(xlsf_type, identity):
        ''' XLSForm Meta field value '''
        if xlsf_type in ('deviceid', 'subscriberid', 'imei'):
            return NA_VALUE
        elif xlsf_type in ('start', 'end'):
            return datetime.now().isoformat()
        elif xlsf_type == 'today':
            return date.today().isoformat()
        elif xlsf_type == 'phonenumber':
            return identity
        return NA_VALUE

    # holder for all properly formated answers
    survey_answers = {}
    # list of (name, data) tuples for media contents
    medias = []
    # keep track of required questions
    notes = []

    # loop on all XLSForm questions
    for expected_group in json_survey.get('children', [{}]):
        if not expected_group.get('type') == 'group':
            # non-grouped questions are not valid for SMS
            continue

        # retrieve part of SMS text for this group
        group_id = expected_group.get('sms_field')
        answers = groups.get(group_id)
        if not group_id or (not answers and not group_id.startswith('meta')):
            # group is not meant to be filled by SMS
            # or hasn't been filled
            continue

        # Add a holder for this group's answers data
        survey_answers.update({expected_group.get('name'): {}})

        # retrieve question definition for each answer
        egroups = expected_group.get('children', [{}])

        # number of intermediate, omited questions (medias)
        step_back = 0
        for idx, question in enumerate(egroups):

            real_value = None

            question_type = question.get('type')
            if question_type in ('calculate'):
                # 'calculate' question are not implemented.
                # 'note' ones are just meant to be displayed on device
                continue

            if question_type == 'note':
                if not question.get('constraint', ''):
                    notes.append(question.get('label'))
                continue

            if not allow_media and question_type in MEDIA_TYPES:
                # if medias for SMS has not been explicitly allowed
                # they are considered excluded.
                step_back += 1
                continue

            # pop the number of skipped questions
            # so that out index is valid even if the form
            # contain medias questions (and medias are disabled)
            sidx = idx - step_back

            if question_type in META_FIELDS:
                # some question are not to be fed by users
                real_value = get_meta_value(xlsf_type=question_type,
                                            identity=identity)
            else:
                # actual SMS-sent answer.
                # Only last answer/question of each group is allowed
                # to have multiple spaces
                if is_last(idx, egroups):
                    answer = u" ".join(answers[idx:])
                else:
                    answer = answers[sidx]

            if real_value is None:
                # retrieve actual value and fail if it doesn't meet reqs.
                real_value = cast_sms_value(answer,
                                            question=question, medias=medias)

            # set value to its question name
            survey_answers[expected_group.get('name')] \
                .update({question.get('name'): real_value})

    return survey_answers, medias, notes