def handle(self, text): # expected format: # # confirm books gwaai valley primary 4288 G # | | | | | # V | | | | # handler V | | | # commodity | | | # V | | # school name | | # | | # | | # V | # school code | # | # | # V # status # declare variables intended for valid information known_contact = None commodity = None quantity = None condition = None observed_cargo = None facility = None school = None possible_schools = [] possible_by_code = None possible_by_name = None school_by_code = None school_by_name = None if self.msg.contact is not None: self.debug(self.msg.contact) known_contact = self.msg.contact elif self.msg.connection.identity is not None: try: known_contact = Contact.objects.get(phone=self.msg.connection.identity) self.msg.connection.contact = known_contact self.msg.connection.save() except MultipleObjectsReturned: #TODO do something? self.debug('MULTIPLE IDENTITIES') pass except ObjectDoesNotExist: self.debug('NO PERSON FOUND') try: known_contact = Contact.objects.get(alternate_phone=\ self.msg.connection.identity) self.msg.connection.contact = known_contact self.msg.connection.save() except MultipleObjectsReturned: #TODO this case may be unneccesary, since many many contacts # often share a single alternate_phone self.debug('MULTIPLE IDENTITIES AFTER UNKNOWN') pass except ObjectDoesNotExist: #self.respond("Sorry, I don't recognize your phone number. Please respond with your surname, facility (school or DEO) code, and facility name.") pass finally: known_contact, new_contact = Contact.objects.get_or_create(phone=self.msg.connection.identity) else: self.debug('NO IDENTITY') if known_contact is not None: self.debug('KNOWN PERSON') # lists of expected token types and their labels for split_into_tokens expected_tokens = ['word', 'words', 'number', 'word'] token_labels = ['commodity', 'school_name', 'school_code', 'condition'] tokens = utils.split_into_tokens(expected_tokens, token_labels, text) self.debug(tokens) if not tokens['commodity'].isdigit(): def get_commodity(token): try: # lookup commodity by slug com = Commodity.objects.get(slug__istartswith=tokens['commodity']) return com except MultipleObjectsReturned: #TODO do something here? pass except ObjectDoesNotExist: coms = Commodity.objects.all() for com in coms: # iterate all commodities and see if submitted # token is in an aliases list match = com.has_alias(token) if match is not None: if match: return com continue return None commodity = get_commodity(tokens['commodity']) if commodity is None: self.respond("Sorry, no supply called '%s'" % (tokens['commodity'])) self.respond("Approved supplies are %s" % ", ".join(Commodity.objects.values_list('slug', flat=True))) if not tokens['school_name'].isdigit(): try: # first try to match name exactly school = School.objects.get(name__iexact=tokens['school_name']) facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) except MultipleObjectsReturned: # if there are many exact matches, add them to the possible_schools list schools = School.objects.filter(name__istartswith=tokens['school_name']) for school in schools: possible_schools.append(school) except ObjectDoesNotExist: # try to match using string distance algorithms possible_by_name = School.closest_by_spelling(tokens['school_name']) self.debug("%s possible facilities by name" % (str(len(possible_by_name)))) self.debug(possible_by_name) if len(possible_by_name) == 1: if possible_by_name[0][2] == 0 and possible_by_name[0][3] == 0 and possible_by_name[0][4] == 1.0: self.debug('PERFECT LOC MATCH BY NAME') school_by_name = possible_by_name[0][1] school = school_by_name facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) else: if possible_by_name is not None: for fac in possible_by_name: # add any non-perfect matches to possible_schools possible_schools.append(fac[1]) if tokens['school_code'].isdigit(): possible_by_code = School.closest_by_code(tokens['school_code']) self.debug("%s possible facilities by code" % (str(len(possible_by_code)))) if len(possible_by_code) == 1: if possible_by_code[0][2] == 0 and possible_by_code[0][3] == 0 and possible_by_code[0][4] == 1.0: self.debug('PERFECT LOC MATCH BY CODE') school_by_code = possible_by_code[0][1] # see if either facility lookup returned a perfect match if school_by_code or school_by_name is not None: if school_by_code and school_by_name is not None: # if they are both the same perfect match we have a winner if school_by_code.pk == school_by_name.pk: school = school_by_code facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) # if we have two different perfect matches, add to list else: possible_schools.append(school_by_code) self.debug("%s possible facilities" % (str(len(possible_schools)))) possible_facilities.append(school_by_name) self.debug("%s possible facilities" % (str(len(possible_schools)))) else: # perfect match by either is also considered a winner school = school_by_code if school_by_code is not None else school_by_name facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) self.debug(facility) # neither lookup returned a perfect match else: # make list of facility objects that are in both fac_by_code and fac_by_name if possible_by_code and possible_by_name is not None: possible_schools.extend([l[1] for l in filter(lambda x:x in possible_by_code, possible_by_name)]) self.debug("%s possible facilities by both" % (str(len(possible_schools)))) if len(possible_schools) == 0: possible_schools.extend([l[1] for l in possible_by_code if possible_by_code is not None]) possible_schools.extend([l[1] for l in possible_by_name if possible_by_name is not None]) self.debug("%s possible facilities by both" % (str(len(possible_schools)))) if len(possible_schools) == 1: school = possible_schools[0] facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) if facility is None: self.respond("Sorry I don't know '%s'" % (tokens['school_name'])) self.respond("Did you mean one of: %s?" %\ (", ".join(possible_schools))) if facility is not None: if not tokens['condition'].isdigit(): if facility is not None: active_shipment = Facility.get_active_shipment(facility) if active_shipment is not None: # create a new Cargo object condition = tokens['condition'].upper() if condition in ['G', 'D', 'L', 'I']: observed_cargo = Cargo.objects.create(\ commodity=commodity,\ condition=condition) else: self.respond("Oops. Status must be one of: G, D, L, or I") seen_by_str = self.msg.connection.backend.name + ":" + self.msg.connection.identity # create a new ShipmentSighting sighting = ShipmentSighting.objects.create(\ observed_cargo=observed_cargo,\ facility=facility, seen_by=seen_by_str) # associate new Cargo with Shipment active_shipment.status = 'D' active_shipment.actual_delivery_time=datetime.datetime.now() active_shipment.cargos.add(observed_cargo) active_shipment.save() # get or create a ShipmentRoute and associate # with new ShipmentSighting route, new_route = ShipmentRoute.objects.get_or_create(\ shipment=active_shipment) route.sightings.add(sighting) route.save() if observed_cargo.condition is not None: this_school = School.objects.get(pk=facility.location_id) # map reported condition to the status numbers # that the sparklines will use map = {'G':1, 'D':-2, 'L':-3, 'I':-4} if observed_cargo.condition in ['D', 'L', 'I', 'G']: this_school.status = map[observed_cargo.condition] else: this_school.status = 0 this_school.save() this_district = this_school.parent # TODO optimize! this is very expensive # and way too slow # re-generate the list of statuses that # the sparklines will use updated = this_district.spark campaign = Campaign.get_active_campaign() if campaign is not None: campaign.shipments.add(active_shipment) campaign.save() data = [ "of %s" % (commodity.slug or "??"), "to %s" % (facility.location.name or "??"), "in %s condition" % (observed_cargo.get_condition_display() or "??") ] confirmation = "Thanks. Confirmed delivery of %s." %\ (" ".join(data)) self.respond(confirmation)
def handle(self, text): # expected format: # # recieved books 123450 4 1 # | | | | | | # V | | | | | # handler V | | | | # commodity | | | | # V | | | # school code | | | # V | | # satellite # | | # V | # # of units | # V # condition code # declare variables intended for valid information known_contact = None commodity = None facility = None quantity = None condition = None observed_cargo = None if self.msg.contact is not None: self.debug(self.msg.contact) known_contact = self.msg.contact elif self.msg.connection.identity is not None: try: known_contact = Contact.objects.get(phone=self.msg.connection.identity) self.msg.connection.contact = known_contact self.msg.connection.save() except MultipleObjectsReturned: #TODO do something? self.debug('MULTIPLE IDENTITIES') pass except ObjectDoesNotExist: self.debug('NO PERSON FOUND') try: known_contact = Contact.objects.get(alternate_phone=\ self.msg.connection.identity) self.msg.connection.contact = known_contact self.msg.connection.save() except MultipleObjectsReturned: #TODO this case may be unneccesary, since many many contacts # often share a single alternate_phone self.debug('MULTIPLE IDENTITIES AFTER UNKNOWN') pass except ObjectDoesNotExist: #self.respond("Sorry, I don't recognize your phone number. Please respond with your surname, facility (school or DEO) code, and facility name.") pass finally: known_contact = Contact.objects.create(phone=self.msg.connection.identity) else: self.debug('NO IDENTITY') if known_contact is not None: self.debug('KNOWN PERSON') expected_tokens = ['word', 'number', 'number', 'number'] token_labels = ['commodity', 'school_code', 'quantity', 'condition'] tokens = utils.split_into_tokens(expected_tokens, token_labels, text) self.debug(tokens) if not tokens['commodity'].isdigit(): def get_commodity(token): try: # lookup commodity by slug com = Commodity.objects.get(slug__istartswith=tokens['commodity']) return com except MultipleObjectsReturned: #TODO do something here? pass except ObjectDoesNotExist: coms = Commodity.objects.all() for com in coms: # iterate all commodities and see if submitted # token is in an aliases list match = com.has_alias(token) if match is not None: if match: return com continue return None commodity = get_commodity(tokens['commodity']) if commodity is None: self.respond("Sorry, no supply called '%s'" % (tokens['commodity'])) self.respond("Approved supplies are %s" % ", ".join(Commodity.objects.values_list('slug', flat=True))) if tokens['school_code'].isdigit(): def list_possible_schools_for_code(school_num): possible_schools = School.objects.filter(code=school_num) if not possible_schools: return None else: # format a list containing # 1) combined school code + satellite_number # 2) school name in parentheses clean_list = [] for school in possible_schools: clean_list.append(school.full_code + " (" + school.name + ")") return clean_list # school code should be between 1 and 5 digits, # and satellite_number should be 1 digit. # in the interest of not hardcoding anything, lets hit the db! max_codes = School.objects.aggregate(max_code=Max('code'),\ max_sat=Max('satellite_number')) max_code_length = len(str(max_codes['max_code'])) max_sat_length = len(str(max_codes['max_sat'])) if len(tokens['school_code']) <= (max_code_length + max_sat_length): # separate school's code and satellite_number (last digit) school_num = tokens['school_code'][:-1] sat_num = tokens['school_code'][-1:] try: school = School.objects.get(code=school_num,\ satellite_number=sat_num) facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) except ObjectDoesNotExist: try: school = School.objects.get(code=tokens['school_code']) facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) except ObjectDoesNotExist: self.respond("Sorry, cannot find school with code '%s'" % (tokens['school_code'])) # maybe satellite number is omitted, so lookup schools by entire token suggestions = list_possible_schools_for_code(tokens['school_code']) if suggestions is not None: self.respond("Did you mean one of: %s?" %\ (", ".join(suggestions))) # maybe satellite number is incorrect, so lookup schools only by school_code suggestions = list_possible_schools_for_code(school_num) if suggestions is not None: self.respond("Did you mean one of: %s?" %\ (", ".join(suggestions))) else: self.respond("Sorry code '%s' is not valid. All codes are fewer than 6 digits" % (tokens['school_code'])) #TODO acceptible values should be configurable #if int(tokens['quantity']) in range(1,10): # if int(tokens['condition']) in range(1,4): if tokens['quantity'].isdigit(): if tokens['condition'].isdigit(): # map expected condition tokens into choices for db conditions_map = {'1':'G', '2':'D', '3':'L'} if facility is not None: active_shipment = Facility.get_active_shipment(facility) if active_shipment is not None: # create a new Cargo object observed_cargo = Cargo.objects.create(\ commodity=commodity,\ quantity=int(tokens['quantity']),\ condition=conditions_map[tokens['condition']]) # create a new ShipmentSighting sighting = ShipmentSighting.objects.create(\ observed_cargo=observed_cargo,\ facility=facility) # associate new Cargo with Shipment active_shipment.status = 'D' active_shipment.actual_delivery_time=datetime.datetime.now() active_shipment.cargos.add(observed_cargo) active_shipment.save() # get or create a ShipmentRoute and associate # with new ShipmentSighting route, new_route = ShipmentRoute.objects.get_or_create(\ shipment=active_shipment) route.sightings.add(sighting) route.save() campaign = Campaign.get_active_campaign() if campaign is not None: campaign.shipments.add(active_shipment) campaign.save() data = [ "%s pallets" % (observed_cargo.quantity or "??"), "of %s" % (commodity.slug or "??"), "to %s" % (facility.location.name or "??"), "in %s condition" % (observed_cargo.get_condition_display() or "??") ] confirmation = "Thanks. Confirmed delivery of %s." %\ (" ".join(data)) self.respond(confirmation)
def active_shipment(self): ''' Return a shipment destined to this school's corresponding Facility object. ''' facility = self.facility() return Facility.get_active_shipment(facility)
def go(): print datetime.datetime.now().isoformat() clean_db = True if clean_db: schools = School.objects.all().update(status=0) print "reset schools" districts = District.objects.all().update(status=None) print "reset districts" shipments = Shipment.objects.all().update(status='P') shipments = Shipment.objects.all().update(actual_delivery_time=None) print "reset shipments" confirmations = Confirmation.objects.all().delete() print "deleted confirmations" sightings = ShipmentSighting.objects.all().delete() print "deleted sightings" routes = ShipmentRoute.objects.all().delete() print "deleted routes" cargos = Cargo.objects.all().delete() print "deleted cargos" incoming = Message.objects.filter(direction='I') unique_text = [] unique = [] # make list of unique incoming messages, based on the message text for mess in incoming: if mess.text not in unique_text: unique_text.append(mess.text) unique.append(mess) # all school names, split into individual words, # flattened into 1-d list, duplicates removed school_name_words = list(set(list(itertools.chain.from_iterable([n.split() for n in School.objects.all().values_list('name', flat=True)])))) # odd punctuation we want to get rid of junk = ['.', ',', '\'', '\"', '`', '(', ')', ':', ';', '&', '?', '!', '~', '`', '+', '-'] school_name_words_no_punc = [] for mark in junk: for word in school_name_words: # remove punctuation from school name words, because we'll be # removing the same punctuation from message text school_name_words_no_punc.append(word.replace(mark, " ")) # if user has spelled out any of the conditions, we want to see those, # as well as "L" -- other conditions "G", "D", "I" already appear in school_name_words_no_punc other_words = ["INCOMPLETE", "GOOD", "DAMAGED", "ALTERNATE", "LOCATION", "L"] ok_words = school_name_words_no_punc + other_words print len(unique) counter = 0 matches = 0 #for text in ['CONFIRM BOOKS DADATA PRIMARY 1196i']: #for msg in unique[45:55]: for msg in unique: counter = counter + 1 if counter % 100 == 0: print "loop: %s" % str(counter) text = msg.text text_list = [] # replace any creative punctuation with spaces for mark in junk: text = text.replace(mark, " ") # split the text into chunks around spaces blobs = text.split(" ") for blob in blobs: clean_blob = blob try: if blob[-1:].isalpha() and blob[:-1].isdigit(): # if theres somthing like '1234g' # add as two separate blobs: '1234' and 'g' text_list.append(blob[:-1]) text_list.append(blob[-1:]) # and move on to next blob before # letters_for_numbers might duplicate it incorrectly continue except IndexError: pass for n in range(3): # clean up blobs only if they have a digit in the first few # characters -- so we don't clean up things like user1 try: if blob[n].isdigit(): clean_blob = letters_for_numbers(blob) break except IndexError: # if the blob doesnt have the first few characters, # and there is no digit yet, move on break # add the cleaned blob (or untouched blob) to a running list text_list.append(clean_blob) relevant = [] # now, loop through cleaned words and keep relevant ones for word in text_list: if word.isdigit(): relevant.append(word) continue if word.upper() in ok_words: relevant.append(word) continue # attach list of relevant bits to message confirmation = Confirmation(message=msg) confirmation.token_list = copy.copy(relevant) confirmation.save() # now try to make sense of these tokens consumed = [] unconsumed = [] # generator to yield relevant items in reverse order consume = consume_in_reverse(relevant) condition = None school = None school_by_code = None school_by_spelling = None try: def attempt_consumption_of_condition_and_code(condition, school_by_code): token = consume.next() if condition is None: condition = reconcile_condition(token) if condition is not None: consumed.append(token) else: if token not in unconsumed: unconsumed.append(token) # if the last token (the first we have examined) this time # has been consumed, pop the next-to-last token. # otherwise, we will continue with the last token if token in consumed: token = consume.next() # note this may be a school object or a list of tuples # in the format: # ('token', school_obj, lev_edit_int, dl_edit_int, jw_float) if school_by_code is None: school_by_code = reconcile_school_by_code(token) if school_by_code is not None: consumed.append(token) confirmation.code = token confirmation.save() else: if token not in unconsumed: unconsumed.append(token) if len(consumed) == 2: return condition, school_by_code else: return attempt_consumption_of_condition_and_code(condition, school_by_code) # recursively consume tokens until we have something for condition and school_by_code condition, school_by_code = attempt_consumption_of_condition_and_code(condition, school_by_code) if not isinstance(school_by_code, list): # woo! we have a condition and a single school, this is probably # enough to be sure about the school, so save it as school before # exploding into finding the school name school = school_by_code confirmation.school = school confirmation.save() try: school_name = None # pop the next-to-next-to-last token token = consume.next() # now lets try to get the school name if token in consumed: token = consume.next() school_name = token consumed.append(token) # consume up to five additional tokens and # prepend to school_name token = consume.next() if token.isalpha(): school_name = token + " " + school_name consumed.append(token) else: unconsumed.append(token) token = consume.next() if token.isalpha(): school_name = token + " " + school_name consumed.append(token) else: unconsumed.append(token) token = consume.next() if token.isalpha(): school_name = token + " " + school_name consumed.append(token) else: unconsumed.append(token) token = consume.next() if token.isalpha(): school_name = token + " " + school_name consumed.append(token) else: unconsumed.append(token) token = consume.next() if token.isalpha(): school_name = token + " " + school_name consumed.append(token) else: unconsumed.append(token) except StopIteration: if school_name is not None: school_by_spelling = reconcile_school_by_spelling(school_name.strip()) p_schools = [] if isinstance(school_by_code, list): for s in (t[1] for t in school_by_code): if s not in p_schools: p_schools.append(s) if school_by_spelling is not None: if not isinstance(school_by_spelling, list): if school is not None: if school.code == school_by_spelling.code: pass else: p_schools.append(school_by_spelling) else: school = school_by_spelling else: for s in (t[1] for t in school_by_spelling): if s is not None: if s.code not in [p.code for p in p_schools if p is not None]: p_schools.append(s) else: school = s # if we have no sure match, and a list of possible schools # returned by reconcile_school_by_spelling, try toggling # the word primary if school is None and isinstance(school_by_spelling, list): uschool_name = school_name.upper() if uschool_name.find("PRIMARY") != -1: edited_name = uschool_name.replace("PRIMARY", "") else: edited_name = uschool_name + " PRIMARY" school_by_spelling = reconcile_school_by_spelling(edited_name.strip()) if school_by_spelling is not None: if not isinstance(school_by_spelling, list): if school is not None: if school.code == school_by_spelling.code: pass else: p_schools.append(school_by_spelling) else: school = school_by_spelling else: for s in (t[1] for t in school_by_spelling): if s.code not in [p.code for p in p_schools]: p_schools.append(s) else: school = s if school is None: confirmation.possible_schools = [s.pk for s in p_schools] confirmation.save() if condition is not None: confirmation.condition = condition confirmation.save() if school is not None: if condition is not None: confirmation.condition = condition confirmation.valid = True confirmation.save() matches = matches + 1 if matches % 20 == 0: print "MATCHES: %s out of %s" % (str(matches), str(counter)) print datetime.datetime.now().isoformat() commodity = Commodity.objects.get(slug__istartswith="textbooks") facility, f_created = Facility.objects.get_or_create(location_id=school.pk,\ location_type=ContentType.objects.get(model='school')) if facility is not None: active_shipment = Facility.get_active_shipment(facility) observed_cargo = Cargo.objects.create(\ commodity=commodity,\ condition=condition) seen_by_str = msg.connection.backend.name + ":" + msg.connection.identity # create a new ShipmentSighting sighting = ShipmentSighting.objects.create(\ observed_cargo=observed_cargo,\ facility=facility, seen_by=seen_by_str) # associate new Cargo with Shipment active_shipment.status = 'D' active_shipment.actual_delivery_time=msg.date active_shipment.cargos.add(observed_cargo) active_shipment.save() # get or create a ShipmentRoute and associate # with new ShipmentSighting route, new_route = ShipmentRoute.objects.get_or_create(\ shipment=active_shipment) route.sightings.add(sighting) route.save() if observed_cargo.condition is not None: this_school = School.objects.get(pk=facility.location_id) # map reported condition to the status numbers # that the sparklines will use map = {'G':1, 'D':-2, 'L':-3, 'I':-4} if observed_cargo.condition in ['D', 'L', 'I', 'G']: this_school.status = map[observed_cargo.condition] else: this_school.status = 0 this_school.save() this_district = this_school.parent # TODO optimize! this is very expensive # and way too slow # re-generate the list of statuses that # the sparklines will use #updated = this_district.spark campaign = Campaign.get_active_campaign() if campaign is not None: campaign.shipments.add(active_shipment) campaign.save() ''' data = [ "of %s" % (commodity.slug or "??"), "to %s" % (facility.location.name or "??"), "in %s condition" % (observed_cargo.get_condition_display() or "??") ] confirmation = "Thanks. Confirmed delivery of %s." %\ (" ".join(data)) print seen_by_str + " " + confirmation ''' except StopIteration: continue except Exception, e: print e print counter print matches import ipdb;ipdb.set_trace()