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