From feadf4b1107d913fc01057bf7e21ae7e79bf79ab Mon Sep 17 00:00:00 2001 From: Sanj Date: Tue, 28 Feb 2012 15:31:38 +0530 Subject: [PATCH 01/25] alert a message in case network dies or so on editstops --- chaloBEST/mumbai/models.py | 2 +- chaloBEST/static/js/editstops.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/chaloBEST/mumbai/models.py b/chaloBEST/mumbai/models.py index 9a75db7..dcae8e9 100644 --- a/chaloBEST/mumbai/models.py +++ b/chaloBEST/mumbai/models.py @@ -123,7 +123,7 @@ class Stop(models.Model): } def get_geojson(self, srid=4326): - print srid +# print srid if self.point is not None: geom = json.loads(self.point.transform(srid, True).geojson) else: diff --git a/chaloBEST/static/js/editstops.js b/chaloBEST/static/js/editstops.js index 4772539..c1cf305 100644 --- a/chaloBEST/static/js/editstops.js +++ b/chaloBEST/static/js/editstops.js @@ -223,12 +223,15 @@ var API_BASE = "/1.0/", var geojsonString = JSON.stringify(geojson); //console.log(geojsonString); var url = API_BASE + "stop/" + stop.slug + "?srid=3857"; - $.post(url, {'geojson': geojsonString}, function(response) { + var $postXHR = $.post(url, {'geojson': geojsonString}, function(response) { if (response.errors) { alert("error saving"); } //console.log(response); }, "json"); + $postXHR.fail(function(e) { + alert('failed ' + JSON.stringify(e)); + }); }); return $div; } From 8f3f4b26c802c1021f2ccf039c666daf91489ccd Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 16:31:52 +0530 Subject: [PATCH 02/25] Document the SMS gateway configuration on atlas. --- gateway/README | 11 +++ gateway/run_gateway.sh | 9 ++ gateway/settings.py | 198 +++++++++++++++++++++++++++++++++++++++++ gateway/smsBEST.conf | 5 ++ 4 files changed, 223 insertions(+) create mode 100644 gateway/README create mode 100755 gateway/run_gateway.sh create mode 100755 gateway/settings.py create mode 100644 gateway/smsBEST.conf diff --git a/gateway/README b/gateway/README new file mode 100644 index 0000000..3ed0ae4 --- /dev/null +++ b/gateway/README @@ -0,0 +1,11 @@ +These files make the SMS gateway on atlas go. + +* apt-get install supervisor +* Create a virtualenv in /srv/smsBEST +* Install RapidSMS to the virtualenv: + http://github.com/schuyler/rapidsms +* rapidsms-admin.py startproject gateway +* Put settings.py in /srv/smsBEST/gateway +* Run manage.py syncdb in /srv/smsBEST/gateway +* Put run_gateway.sh in /srv/smsBEST/bin +* Put smsBEST.conf in /etc/supervisor/conf.d diff --git a/gateway/run_gateway.sh b/gateway/run_gateway.sh new file mode 100755 index 0000000..d9686d6 --- /dev/null +++ b/gateway/run_gateway.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# +# Launch the router. Put this file in /srv/smsBEST/bin. +# +cd /srv/smsBEST +source bin/activate +cd gateway +python manage.py runrouter + diff --git a/gateway/settings.py b/gateway/settings.py new file mode 100755 index 0000000..5cef581 --- /dev/null +++ b/gateway/settings.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# vim: ai ts=4 sts=4 et sw=4 +# encoding=utf-8 + +# Put this in /srv/smsBEST/gateway and change the gateway secret. + +# -------------------------------------------------------------------- # +# MAIN CONFIGURATION # +# -------------------------------------------------------------------- # + + +# you should configure your database here before doing any real work. +# see: http://docs.djangoproject.com/en/dev/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "rapidsms.sqlite3", + } +} + + +# the rapidsms backend configuration is designed to resemble django's +# database configuration, as a nested dict of (name, configuration). +# +# the ENGINE option specifies the module of the backend; the most common +# backend types (for a GSM modem or an SMPP server) are bundled with +# rapidsms, but you may choose to write your own. +# +# all other options are passed to the Backend when it is instantiated, +# to configure it. see the documentation in those modules for a list of +# the valid options for each. +INSTALLED_BACKENDS = { + "modem": { + "ENGINE": "rapidsms.backends.gsm", + "PORT": "/dev/ttyS0", + "baudrate": 115200, + "timeout": 10 + }, + #"verizon": { + # "ENGINE": "rapidsms.backends.gsm, + # "PORT": "/dev/ttyUSB1" + #}, + "message_tester": { + "ENGINE": "rapidsms.backends.bucket", + } +} + +GATEWAY = { + "backend": "modem", + "secret": "something secret", + "push": "http://chalobest.in:8086/?from=%(from)s&txt=%(txt)s&secret=%(secret)s" +} + + +# to help you get started quickly, many django/rapidsms apps are enabled +# by default. you may wish to remove some and/or add your own. +INSTALLED_APPS = [ + + # the essentials. + "django_nose", + "djtables", + "rapidsms", + + # common dependencies (which don't clutter up the ui). + "rapidsms.contrib.handlers", + "rapidsms.contrib.ajax", + + # enable the django admin using a little shim app (which includes + # the required urlpatterns), and a bunch of undocumented apps that + # the AdminSite seems to explode without. + "django.contrib.sites", + "django.contrib.auth", + "django.contrib.admin", + "django.contrib.sessions", + "django.contrib.contenttypes", + + # the rapidsms contrib apps. +# "rapidsms.contrib.default", + "rapidsms.contrib.export", + "rapidsms.contrib.httptester", +# "rapidsms.contrib.locations", + "rapidsms.contrib.messagelog", + "rapidsms.contrib.messaging", + "rapidsms.contrib.gateway", +# "rapidsms.contrib.registration", +# "rapidsms.contrib.scheduler", +# "rapidsms.contrib.echo", +] + + +# this rapidsms-specific setting defines which views are linked by the +# tabbed navigation. when adding an app to INSTALLED_APPS, you may wish +# to add it here, also, to expose it in the rapidsms ui. +RAPIDSMS_TABS = [ + ("rapidsms.contrib.messagelog.views.message_log", "Message Log"), +# ("rapidsms.contrib.registration.views.registration", "Registration"), + ("rapidsms.contrib.messaging.views.messaging", "Messaging"), +# ("rapidsms.contrib.locations.views.locations", "Map"), +# ("rapidsms.contrib.scheduler.views.index", "Event Scheduler"), + ("rapidsms.contrib.httptester.views.generate_identity", "Message Tester"), +] + + +# -------------------------------------------------------------------- # +# BORING CONFIGURATION # +# -------------------------------------------------------------------- # + + +# debug mode is turned on as default, since rapidsms is under heavy +# development at the moment, and full stack traces are very useful +# when reporting bugs. don't forget to turn this off in production. +DEBUG = TEMPLATE_DEBUG = True + + +# after login (which is handled by django.contrib.auth), redirect to the +# dashboard rather than 'accounts/profile' (the default). +LOGIN_REDIRECT_URL = "/" + + +# use django-nose to run tests. rapidsms contains lots of packages and +# modules which django does not find automatically, and importing them +# all manually is tiresome and error-prone. +TEST_RUNNER = "django_nose.NoseTestSuiteRunner" + + +# for some reason this setting is blank in django's global_settings.py, +# but it is needed for static assets to be linkable. +MEDIA_URL = "/static/" + + +# this is required for the django.contrib.sites tests to run, but also +# not included in global_settings.py, and is almost always ``1``. +# see: http://docs.djangoproject.com/en/dev/ref/contrib/sites/ +SITE_ID = 1 + + +# the default log settings are very noisy. +LOG_LEVEL = "DEBUG" +LOG_FILE = "rapidsms.log" +LOG_FORMAT = "[%(name)s]: %(message)s" +LOG_SIZE = 8192 # 8192 bits = 8 kb +LOG_BACKUPS = 256 # number of logs to keep + + +# these weird dependencies should be handled by their respective apps, +# but they're not, so here they are. most of them are for django admin. +TEMPLATE_CONTEXT_PROCESSORS = [ + "django.core.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.request", +] + +# template loaders load templates from various places. +# for djtables to work properly, the egg loader needs to be +# included, the others are fairly standard. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'django.template.loaders.eggs.Loader' +) + +# -------------------------------------------------------------------- # +# HERE BE DRAGONS! # +# these settings are pure hackery, and will go away soon # +# -------------------------------------------------------------------- # + + +# these apps should not be started by rapidsms in your tests, however, +# the models and bootstrap will still be available through django. +TEST_EXCLUDED_APPS = [ + "django.contrib.sessions", + "django.contrib.contenttypes", + "django.contrib.auth", + "rapidsms", + "rapidsms.contrib.ajax", + "rapidsms.contrib.httptester", +] + +# the project-level url patterns +ROOT_URLCONF = "urls" + + +# since we might hit the database from any thread during testing, the +# in-memory sqlite database isn't sufficient. it spawns a separate +# virtual database for each thread, and syncdb is only called for the +# first. this leads to confusing "no such table" errors. We create +# a named temporary instance instead. +import os +import tempfile +import sys + +if 'test' in sys.argv: + for db_name in DATABASES: + DATABASES[db_name]['TEST_NAME'] = os.path.join( + tempfile.gettempdir(), + "%s.rapidsms.test.sqlite3" % db_name) diff --git a/gateway/smsBEST.conf b/gateway/smsBEST.conf new file mode 100644 index 0000000..003bf59 --- /dev/null +++ b/gateway/smsBEST.conf @@ -0,0 +1,5 @@ +# this file goes in /etc/supervisor/conf.d +[program:smsBEST] +command=/srv/smsBEST/bin/run_gateway.sh +directory=/srv/smsBEST/var +autorestart=true From ae76b5cdd1477df5e7a1b20a441149a046628343 Mon Sep 17 00:00:00 2001 From: Sanj Date: Tue, 28 Feb 2012 17:25:14 +0530 Subject: [PATCH 03/25] remove selectedStopItem css class onFeatureUnselect --- chaloBEST/static/js/editstops.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chaloBEST/static/js/editstops.js b/chaloBEST/static/js/editstops.js index c1cf305..21334f3 100644 --- a/chaloBEST/static/js/editstops.js +++ b/chaloBEST/static/js/editstops.js @@ -327,6 +327,7 @@ var API_BASE = "/1.0/", 'coordinates': [e.feature.geometry.x, e.feature.geometry.y] }; var $form = getStopForm(stop, geom); + $('.selectedStopItem').removeClass("selectedStopItem"); $('#stopForm').remove(); $('#formCol').empty(); $('#formCol').append($form); From e345ea7a63ff9c35c3a4f3a0bc2719ade20b62e9 Mon Sep 17 00:00:00 2001 From: Sanj Date: Tue, 28 Feb 2012 17:52:02 +0530 Subject: [PATCH 04/25] actually fix double selection bug --- chaloBEST/static/js/editstops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chaloBEST/static/js/editstops.js b/chaloBEST/static/js/editstops.js index 21334f3..278d2a5 100644 --- a/chaloBEST/static/js/editstops.js +++ b/chaloBEST/static/js/editstops.js @@ -321,13 +321,13 @@ var API_BASE = "/1.0/", // console.log(e.feature); var slug = e.feature.attributes.slug; //alert("selected " + slug); + $('.selectedStop').removeClass("selectedStop"); highlightStop(slug); var stop = e.feature.attributes; var geom = { 'coordinates': [e.feature.geometry.x, e.feature.geometry.y] }; var $form = getStopForm(stop, geom); - $('.selectedStopItem').removeClass("selectedStopItem"); $('#stopForm').remove(); $('#formCol').empty(); $('#formCol').append($form); From b5fabf412cf55b83a0ceda7991ace83b983ad0a0 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 18:14:19 +0530 Subject: [PATCH 05/25] Add TrigramSearchManager to ChaloBEST models. --- chaloBEST/mumbai/models.py | 21 +++++++++++++++++++++ gateway/settings.py | 3 +-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/chaloBEST/mumbai/models.py b/chaloBEST/mumbai/models.py index dcae8e9..e5e81a4 100644 --- a/chaloBEST/mumbai/models.py +++ b/chaloBEST/mumbai/models.py @@ -42,7 +42,27 @@ SCHED = { '2nd &4th':['???'] } +class TrigramSearchManager(models.Manager): + def set_threshold(self, threshold): + """Set the limit for trigram similarity matching.""" + cursor = connection.cursor() + cursor.execute("""SELECT set_limit(%f)""" % threshold) + + def find_approximate(self, match=0.5, **kwargs): + self.set_threshold(match) + assert(len(kwargs) == 1) + column, value = kwargs.items()[0] + qset = self.get_query_set() + # use the pg_trgm index via the % operator + qset = qset.extra(select={"similarity":"similarity(" + column + ", %s)"}, + select_params=[value], + where=[column + " %% %s"], + params=[value], + order_by=["-similarity"]) + return qset + class Area(models.Model): + objects = TrigramSearchManager() # name, name_mr code = models.IntegerField() #primary_key=True) slug = models.SlugField(null=True) name = models.TextField(blank=True, max_length=255) @@ -93,6 +113,7 @@ class Fare(models.Model): class Stop(models.Model): + objects = TrigramSearchManager() # name, display, name_mr code = models.IntegerField() slug = models.SlugField(null=True) name = models.TextField(blank=True, max_length=255) diff --git a/gateway/settings.py b/gateway/settings.py index 5cef581..ce79173 100755 --- a/gateway/settings.py +++ b/gateway/settings.py @@ -2,8 +2,6 @@ # vim: ai ts=4 sts=4 et sw=4 # encoding=utf-8 -# Put this in /srv/smsBEST/gateway and change the gateway secret. - # -------------------------------------------------------------------- # # MAIN CONFIGURATION # # -------------------------------------------------------------------- # @@ -51,6 +49,7 @@ GATEWAY = { "push": "http://chalobest.in:8086/?from=%(from)s&txt=%(txt)s&secret=%(secret)s" } +AJAX_PROXY_HOST = "0.0.0.0" # to open the gateway from the outside # to help you get started quickly, many django/rapidsms apps are enabled # by default. you may wish to remove some and/or add your own. From 0cf8fae70bf4aa36843d90764ad8fb341006e954 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 18:55:35 +0530 Subject: [PATCH 06/25] Somewhat re-thought TrigramSearchManager. --- chaloBEST/mumbai/models.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/chaloBEST/mumbai/models.py b/chaloBEST/mumbai/models.py index e5e81a4..da00dc0 100644 --- a/chaloBEST/mumbai/models.py +++ b/chaloBEST/mumbai/models.py @@ -43,26 +43,32 @@ SCHED = { } class TrigramSearchManager(models.Manager): + def __init__(self, trigram_columns, *args, **kwargs): + super(models.Manager, self).__init__(*args, **kwargs) + self.trigram_columns = trigram_columns + def set_threshold(self, threshold): """Set the limit for trigram similarity matching.""" cursor = connection.cursor() cursor.execute("""SELECT set_limit(%f)""" % threshold) - def find_approximate(self, match=0.5, **kwargs): + def find_approximate(self, text, match=0.5): self.set_threshold(match) - assert(len(kwargs) == 1) - column, value = kwargs.items()[0] + similarity_measure = "max(%s)" % ",".join(["similarity(%s, %%s)" % col for col in self.trigram_columns]) + similarity_filter = " OR ".join(["%s %%%% %%s" % col for col in self.trigram_columns]) + text_values = [text] * len(self.trigram_columns) + qset = self.get_query_set() # use the pg_trgm index via the % operator - qset = qset.extra(select={"similarity":"similarity(" + column + ", %s)"}, - select_params=[value], - where=[column + " %% %s"], - params=[value], + qset = qset.extra(select={"similarity":similarity_measure, + select_params=text_values, + where=similarity_filter, + params=text_values, order_by=["-similarity"]) return qset class Area(models.Model): - objects = TrigramSearchManager() # name, name_mr + objects = TrigramSearchManager("name", "name_mr", "display_name") code = models.IntegerField() #primary_key=True) slug = models.SlugField(null=True) name = models.TextField(blank=True, max_length=255) @@ -111,9 +117,8 @@ class Fare(models.Model): def __unicode__(self): return str(self.slab) - class Stop(models.Model): - objects = TrigramSearchManager() # name, display, name_mr + objects = TrigramSearchManager("name", "name_mr", "display_name") code = models.IntegerField() slug = models.SlugField(null=True) name = models.TextField(blank=True, max_length=255) From ddfbe2646ece231cfce0a35d751433d998aa691f Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 19:00:24 +0530 Subject: [PATCH 07/25] Somewhat re-thought TrigramSearchManager (using greatest instead of max). --- chaloBEST/mumbai/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chaloBEST/mumbai/models.py b/chaloBEST/mumbai/models.py index da00dc0..414effc 100644 --- a/chaloBEST/mumbai/models.py +++ b/chaloBEST/mumbai/models.py @@ -54,7 +54,7 @@ class TrigramSearchManager(models.Manager): def find_approximate(self, text, match=0.5): self.set_threshold(match) - similarity_measure = "max(%s)" % ",".join(["similarity(%s, %%s)" % col for col in self.trigram_columns]) + similarity_measure = "greatest(%s)" % ",".join(["similarity(%s, %%s)" % col for col in self.trigram_columns]) similarity_filter = " OR ".join(["%s %%%% %%s" % col for col in self.trigram_columns]) text_values = [text] * len(self.trigram_columns) From e8e5b6e85a7bb6a58b25865b8b0632238c774a8f Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 19:14:47 +0530 Subject: [PATCH 08/25] Add mgmt command to create trigram indexes. --- chaloBEST/mumbai/management/__init__.py | 0 .../mumbai/management/commands/__init__.py | 0 .../mumbai/management/commands/trgmidx.py | 18 ++++++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 chaloBEST/mumbai/management/__init__.py create mode 100644 chaloBEST/mumbai/management/commands/__init__.py create mode 100644 chaloBEST/mumbai/management/commands/trgmidx.py diff --git a/chaloBEST/mumbai/management/__init__.py b/chaloBEST/mumbai/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chaloBEST/mumbai/management/commands/__init__.py b/chaloBEST/mumbai/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chaloBEST/mumbai/management/commands/trgmidx.py b/chaloBEST/mumbai/management/commands/trgmidx.py new file mode 100644 index 0000000..000e0d7 --- /dev/null +++ b/chaloBEST/mumbai/management/commands/trgmidx.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand, CommandError +from django.db import connection +from mumbai import models + +class Command(BaseCommand): + help = "Instantiates the pg_trgm indexes" + + def handle(self, *args, **options): + cursor = connection.cursor() + for name, model in models: + if not hasattr(model, "objects") or \ + not isinstance(model.objects, model.TrigramSearchManager): + continue + table = model._meta.db_table + for column in model.objects.trigram_columns: + cursor.execute(""" + CREATE INDEX %s_%s_trgm_idx ON %s USING gin (%s gin_trgm_ops);""" % ( + table, column, table, column)) From 44e91fe2cafa3b611964c2490d1d935dfab9709b Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 07:00:28 -0800 Subject: [PATCH 09/25] Fix the dummheiten in the TrigramSearchManager. --- chaloBEST/mumbai/management/commands/trgmidx.py | 12 +++++++----- chaloBEST/mumbai/models.py | 14 +++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/chaloBEST/mumbai/management/commands/trgmidx.py b/chaloBEST/mumbai/management/commands/trgmidx.py index 000e0d7..b4af2d3 100644 --- a/chaloBEST/mumbai/management/commands/trgmidx.py +++ b/chaloBEST/mumbai/management/commands/trgmidx.py @@ -7,12 +7,14 @@ class Command(BaseCommand): def handle(self, *args, **options): cursor = connection.cursor() - for name, model in models: + for name in dir(models): + model = getattr(models, name) if not hasattr(model, "objects") or \ - not isinstance(model.objects, model.TrigramSearchManager): + not isinstance(model.objects, models.TrigramSearchManager): continue table = model._meta.db_table for column in model.objects.trigram_columns: - cursor.execute(""" - CREATE INDEX %s_%s_trgm_idx ON %s USING gin (%s gin_trgm_ops);""" % ( - table, column, table, column)) + sql = """CREATE INDEX %s_%s_trgm_idx ON %s USING gin (%s gin_trgm_ops);""" % ( + table, column, table, column) + cursor.execute(sql) + cursor.execute("COMMIT;") diff --git a/chaloBEST/mumbai/models.py b/chaloBEST/mumbai/models.py index 414effc..bae603d 100644 --- a/chaloBEST/mumbai/models.py +++ b/chaloBEST/mumbai/models.py @@ -3,6 +3,7 @@ from django.contrib.gis.geos import Point from django import forms from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic +from django.db import connection import json STOP_CHOICES = ( ('U','Up'), @@ -43,8 +44,8 @@ SCHED = { } class TrigramSearchManager(models.Manager): - def __init__(self, trigram_columns, *args, **kwargs): - super(models.Manager, self).__init__(*args, **kwargs) + def __init__(self, trigram_columns=[]): + super(TrigramSearchManager, self).__init__() self.trigram_columns = trigram_columns def set_threshold(self, threshold): @@ -57,18 +58,17 @@ class TrigramSearchManager(models.Manager): similarity_measure = "greatest(%s)" % ",".join(["similarity(%s, %%s)" % col for col in self.trigram_columns]) similarity_filter = " OR ".join(["%s %%%% %%s" % col for col in self.trigram_columns]) text_values = [text] * len(self.trigram_columns) - qset = self.get_query_set() # use the pg_trgm index via the % operator - qset = qset.extra(select={"similarity":similarity_measure, + qset = qset.extra(select={"similarity":similarity_measure}, select_params=text_values, - where=similarity_filter, + where=[similarity_filter], params=text_values, order_by=["-similarity"]) return qset class Area(models.Model): - objects = TrigramSearchManager("name", "name_mr", "display_name") + objects = TrigramSearchManager(("name", "name_mr", "display_name")) code = models.IntegerField() #primary_key=True) slug = models.SlugField(null=True) name = models.TextField(blank=True, max_length=255) @@ -118,7 +118,7 @@ class Fare(models.Model): return str(self.slab) class Stop(models.Model): - objects = TrigramSearchManager("name", "name_mr", "display_name") + objects = TrigramSearchManager(("name", "name_mr", "display_name")) code = models.IntegerField() slug = models.SlugField(null=True) name = models.TextField(blank=True, max_length=255) From 08946256bda1ebfdc9e0281075bae849d94852c6 Mon Sep 17 00:00:00 2001 From: Sanj Date: Tue, 28 Feb 2012 20:33:18 +0530 Subject: [PATCH 10/25] silly error - changed route to foreignkey, updated admin --- chaloBEST/mumbai/admin.py | 2 +- chaloBEST/static/js/editstops.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/chaloBEST/mumbai/admin.py b/chaloBEST/mumbai/admin.py index 72ef894..6de00be 100644 --- a/chaloBEST/mumbai/admin.py +++ b/chaloBEST/mumbai/admin.py @@ -51,7 +51,7 @@ class FareAdmin(admin.ModelAdmin): class UniqueRouteAdmin(admin.ModelAdmin): list_display = ("route","from_stop", "to_stop","distance","is_full") readonly_fields = ("route","from_stop", "to_stop","distance","is_full") - search_fields = ("route", "from_stop__name", "to_stop__name") + search_fields = ("route__alias", "from_stop__name", "to_stop__name") ordering = ('route',) list_per_page = 50 diff --git a/chaloBEST/static/js/editstops.js b/chaloBEST/static/js/editstops.js index 278d2a5..9b67392 100644 --- a/chaloBEST/static/js/editstops.js +++ b/chaloBEST/static/js/editstops.js @@ -23,7 +23,7 @@ var API_BASE = "/1.0/", var $li = $('
') .addClass("listItem") .appendTo($list); - var $txt = $('').addClass("listItemText").text(v).appendTo($li); + var $txt = $('').addClass("listItemText").text(v).appendTo($li); }); }); } @@ -51,6 +51,7 @@ var API_BASE = "/1.0/", $('.selectedListItem').find(".stopsList").hide().remove(); $('.selectedListItem').removeClass("selectedListItem"); $target.addClass("selectedListItem"); + if ($target.data("hasList")) { var $stopsList = $target.find(".stopsList"); $stopsList.slideDown(); From 655e95d7cd2e2bdbcec44e6138e0ce6ebae05b28 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 08:59:30 -0800 Subject: [PATCH 11/25] Add RapidSMS requirement; create smsBEST RapidSMS project. --- requirements.txt | 2 + smsBEST/__init__.py | 0 smsBEST/manage.py | 18 ++++ smsBEST/mumbai/__init__.py | 0 smsBEST/mumbai/app.py | 4 + smsBEST/mumbai/views.py | 1 + smsBEST/settings.py | 205 +++++++++++++++++++++++++++++++++++++ smsBEST/urls.py | 37 +++++++ 8 files changed, 267 insertions(+) create mode 100644 smsBEST/__init__.py create mode 100755 smsBEST/manage.py create mode 100644 smsBEST/mumbai/__init__.py create mode 100644 smsBEST/mumbai/app.py create mode 100644 smsBEST/mumbai/views.py create mode 100644 smsBEST/settings.py create mode 100644 smsBEST/urls.py diff --git a/requirements.txt b/requirements.txt index a239605..226e914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ -e git+git://github.com/bit/django-extensions.git#egg=django_extensions #django_extensions django-grappelli +-e git+git://github.com/schuyler/rapidsms.git#egg=rapidsms +-e git+git://github.com/schuyler/arrest.git#egg=arrest diff --git a/smsBEST/__init__.py b/smsBEST/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smsBEST/manage.py b/smsBEST/manage.py new file mode 100755 index 0000000..b23e297 --- /dev/null +++ b/smsBEST/manage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# vim: ai ts=4 sts=4 et sw=4 + +#import sys, os + +from django.core.management import execute_manager +import settings + + +if __name__ == "__main__": +# project_root = os.path.abspath( +# os.path.dirname(__file__)) + +# path = os.path.join(project_root, "apps") +# sys.path.insert(0, path) + +# sys.path.insert(0, project_root) + execute_manager(settings) diff --git a/smsBEST/mumbai/__init__.py b/smsBEST/mumbai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py new file mode 100644 index 0000000..e87ddb0 --- /dev/null +++ b/smsBEST/mumbai/app.py @@ -0,0 +1,4 @@ +from rapidsms.apps.base import AppBase + +class App(AppBase): + pass diff --git a/smsBEST/mumbai/views.py b/smsBEST/mumbai/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/smsBEST/mumbai/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/smsBEST/settings.py b/smsBEST/settings.py new file mode 100644 index 0000000..fc14793 --- /dev/null +++ b/smsBEST/settings.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# vim: ai ts=4 sts=4 et sw=4 +# encoding=utf-8 + +# -------------------------------------------------------------------- # +# MAIN CONFIGURATION # +# -------------------------------------------------------------------- # + + +# you should configure your database here before doing any real work. +# see: http://docs.djangoproject.com/en/dev/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "smsbest", + } +} + + +# the rapidsms backend configuration is designed to resemble django's +# database configuration, as a nested dict of (name, configuration). +# +# the ENGINE option specifies the module of the backend; the most common +# backend types (for a GSM modem or an SMPP server) are bundled with +# rapidsms, but you may choose to write your own. +# +# all other options are passed to the Backend when it is instantiated, +# to configure it. see the documentation in those modules for a list of +# the valid options for each. +INSTALLED_BACKENDS = { + #"att": { + # "ENGINE": "rapidsms.backends.gsm", + # "PORT": "/dev/ttyUSB0" + #}, + #"verizon": { + # "ENGINE": "rapidsms.backends.gsm, + # "PORT": "/dev/ttyUSB1" + #}, + "atlas": { + "ENGINE": "rapidsms.backends.http", + "host": "0.0.0.0", + "port": 8086, + "gateway_url": "http://atlas.gnowledge.org:8001/gateway/send", + "params_outgoing": "secret=something+secret&to=%(phone_number)s&txt=%(message)s", + "params_incoming": "from=%(phone_number)s&txt=%(message)s" + + }, + "message_tester": { + "ENGINE": "rapidsms.backends.bucket", + } +} + + +# to help you get started quickly, many django/rapidsms apps are enabled +# by default. you may wish to remove some and/or add your own. +INSTALLED_APPS = [ + + # the essentials. + "django_nose", + "djtables", + "rapidsms", + + # common dependencies (which don't clutter up the ui). + "rapidsms.contrib.handlers", + "rapidsms.contrib.ajax", + + # enable the django admin using a little shim app (which includes + # the required urlpatterns), and a bunch of undocumented apps that + # the AdminSite seems to explode without. + "django.contrib.sites", + "django.contrib.auth", + "django.contrib.admin", + "django.contrib.sessions", + "django.contrib.contenttypes", + + # the rapidsms contrib apps. + # "rapidsms.contrib.default", + # "rapidsms.contrib.export", + "rapidsms.contrib.httptester", + # "rapidsms.contrib.locations", + "rapidsms.contrib.messagelog", + "rapidsms.contrib.messaging", + "rapidsms.contrib.registration", + # "rapidsms.contrib.scheduler", + # "rapidsms.contrib.echo", + + "mumbai" +] + + + +# this rapidsms-specific setting defines which views are linked by the +# tabbed navigation. when adding an app to INSTALLED_APPS, you may wish +# to add it here, also, to expose it in the rapidsms ui. +RAPIDSMS_TABS = [ + ("rapidsms.contrib.messagelog.views.message_log", "Message Log"), + ("rapidsms.contrib.registration.views.registration", "Registration"), + ("rapidsms.contrib.messaging.views.messaging", "Messaging"), + # ("rapidsms.contrib.locations.views.locations", "Map"), + # ("rapidsms.contrib.scheduler.views.index", "Event Scheduler"), + ("rapidsms.contrib.httptester.views.generate_identity", "Message Tester"), +] + + +# -------------------------------------------------------------------- # +# BORING CONFIGURATION # +# -------------------------------------------------------------------- # + + +# debug mode is turned on as default, since rapidsms is under heavy +# development at the moment, and full stack traces are very useful +# when reporting bugs. don't forget to turn this off in production. +DEBUG = TEMPLATE_DEBUG = True + + +# after login (which is handled by django.contrib.auth), redirect to the +# dashboard rather than 'accounts/profile' (the default). +LOGIN_REDIRECT_URL = "/" + + +# use django-nose to run tests. rapidsms contains lots of packages and +# modules which django does not find automatically, and importing them +# all manually is tiresome and error-prone. +TEST_RUNNER = "django_nose.NoseTestSuiteRunner" + + +# for some reason this setting is blank in django's global_settings.py, +# but it is needed for static assets to be linkable. +MEDIA_URL = "/static/" + + +# this is required for the django.contrib.sites tests to run, but also +# not included in global_settings.py, and is almost always ``1``. +# see: http://docs.djangoproject.com/en/dev/ref/contrib/sites/ +SITE_ID = 1 + + +# the default log settings are very noisy. +LOG_LEVEL = "DEBUG" +LOG_FILE = "rapidsms.log" +LOG_FORMAT = "[%(name)s]: %(message)s" +LOG_SIZE = 8192 # 8192 bits = 8 kb +LOG_BACKUPS = 256 # number of logs to keep + + +# these weird dependencies should be handled by their respective apps, +# but they're not, so here they are. most of them are for django admin. +TEMPLATE_CONTEXT_PROCESSORS = [ + "django.core.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.request", +] + +# template loaders load templates from various places. +# for djtables to work properly, the egg loader needs to be +# included, the others are fairly standard. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'django.template.loaders.eggs.Loader' +) + +# -------------------------------------------------------------------- # +# HERE BE DRAGONS! # +# these settings are pure hackery, and will go away soon # +# -------------------------------------------------------------------- # + + +# these apps should not be started by rapidsms in your tests, however, +# the models and bootstrap will still be available through django. +TEST_EXCLUDED_APPS = [ + "django.contrib.sessions", + "django.contrib.contenttypes", + "django.contrib.auth", + "rapidsms", + "rapidsms.contrib.ajax", + "rapidsms.contrib.httptester", +] + +# the project-level url patterns +ROOT_URLCONF = "urls" + +# import local_settings.py +try: + from local_settings import * +except: + pass + +# since we might hit the database from any thread during testing, the +# in-memory sqlite database isn't sufficient. it spawns a separate +# virtual database for each thread, and syncdb is only called for the +# first. this leads to confusing "no such table" errors. We create +# a named temporary instance instead. +import os +import tempfile +import sys + +if 'test' in sys.argv: + for db_name in DATABASES: + DATABASES[db_name]['TEST_NAME'] = os.path.join( + tempfile.gettempdir(), + "%s.rapidsms.test.sqlite3" % db_name) + diff --git a/smsBEST/urls.py b/smsBEST/urls.py new file mode 100644 index 0000000..34efec2 --- /dev/null +++ b/smsBEST/urls.py @@ -0,0 +1,37 @@ +from django.conf.urls.defaults import * +from django.conf import settings +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = patterns('', + # Example: + # (r'^my-project/', include('my_project.foo.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + (r'^admin/', include(admin.site.urls)), + + # RapidSMS core URLs + (r'^account/', include('rapidsms.urls.login_logout')), + url(r'^$', 'rapidsms.views.dashboard', name='rapidsms-dashboard'), + + # RapidSMS contrib app URLs + (r'^ajax/', include('rapidsms.contrib.ajax.urls')), + (r'^export/', include('rapidsms.contrib.export.urls')), + (r'^httptester/', include('rapidsms.contrib.httptester.urls')), + (r'^locations/', include('rapidsms.contrib.locations.urls')), + (r'^messagelog/', include('rapidsms.contrib.messagelog.urls')), + (r'^messaging/', include('rapidsms.contrib.messaging.urls')), + (r'^registration/', include('rapidsms.contrib.registration.urls')), + (r'^scheduler/', include('rapidsms.contrib.scheduler.urls')), +) + +if settings.DEBUG: + urlpatterns += patterns('', + # helper URLs file that automatically serves the 'static' folder in + # INSTALLED_APPS via the Django static media server (NOT for use in + # production) + (r'^', include('rapidsms.urls.static_media')), + ) From d04f61ab78b70d969f68f7fd49a4e51ba8af0ae8 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 10:10:22 -0800 Subject: [PATCH 12/25] More or less working mumbai.app --- smsBEST/mumbai/app.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index e87ddb0..1e566a0 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -1,4 +1,21 @@ from rapidsms.apps.base import AppBase +import re +import arrest + +DIGIT = re.compile(r"\d{1,3}") +chalobest = arrest.Client("http://chalobest.in/1.0") class App(AppBase): - pass + def handle(self, msg): + if DIGIT.search(msg.text): + routes = chalobest.routes(q=msg.text.replace(" ", "")) + detail = chalobest.route[routes[0]] + stops = detail['stops']['features'] + origin, destination = stops[0]['properties'], stops[-1]['properties'] + msg.respond("%s: %s (%s) to %s (%s)" % (routes[0], + origin['display_name'], origin['area'], + destination['display_name'], destination['area'])) + else: + stops = chalobest.stops(q=msg.text) + stop = stops['features'][0]['properties'] + msg.respond("%s (%s): %s" % (stop['official_name'], stop['area'], stop['routes'])) From 3503fc42480260647ab5a7ad8f11773bdfbf16f1 Mon Sep 17 00:00:00 2001 From: Sanj Date: Wed, 29 Feb 2012 00:33:12 +0530 Subject: [PATCH 13/25] Route does not use find_approximate --- chaloBEST/mumbai/apiviews.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chaloBEST/mumbai/apiviews.py b/chaloBEST/mumbai/apiviews.py index cfc3506..0106e04 100644 --- a/chaloBEST/mumbai/apiviews.py +++ b/chaloBEST/mumbai/apiviews.py @@ -31,7 +31,7 @@ def area(request, slug): def routes(request): q = request.GET.get("q", "") if q != '': - qset = Route.objects.find_approximate(q, 0.33) + qset = Route.objects.filter(alias__icontains=q) else: qset = Route.objects.all() routes = [route.alias for route in qset] From 357600e1a8d8d55b82dc61d5a86942aa407ade3f Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 11:48:28 -0800 Subject: [PATCH 14/25] Fill up the response buffer on name queries. --- smsBEST/mumbai/app.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index 1e566a0..5031670 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -2,6 +2,7 @@ from rapidsms.apps.base import AppBase import re import arrest +MAX_MSG_LEN = 160 DIGIT = re.compile(r"\d{1,3}") chalobest = arrest.Client("http://chalobest.in/1.0") @@ -16,6 +17,14 @@ class App(AppBase): origin['display_name'], origin['area'], destination['display_name'], destination['area'])) else: - stops = chalobest.stops(q=msg.text) - stop = stops['features'][0]['properties'] - msg.respond("%s (%s): %s" % (stop['official_name'], stop['area'], stop['routes'])) + stops = chalobest.stops(q=msg.text)['features'] + response = "" + for feature in stops: + stop = feature['properties'] + match = "%s (%s): %s" % (stop['official_name'], stop['area'], stop['routes']) + if len(response) + len(match) + 1 < MAX_MSG_LEN: + if response: response += ";" + response += match + else: + break + msg.respond(response) From a75af22f6a021695606530391a4ee83adbc9a6eb Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 12:03:35 -0800 Subject: [PATCH 15/25] bzr merge, dur --- smsBEST/mumbai/app.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index 5031670..32c9ef2 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -4,27 +4,37 @@ import arrest MAX_MSG_LEN = 160 DIGIT = re.compile(r"\d{1,3}") -chalobest = arrest.Client("http://chalobest.in/1.0") +PUNCT = re.compile(r"[^\w\s]") +STYLE = { + "start": "-* ", + "repeat": "-*-", + "end": " *-" +} + + +ChaloBest = arrest.Client("http://chalobest.in/1.0") class App(AppBase): def handle(self, msg): if DIGIT.search(msg.text): - routes = chalobest.routes(q=msg.text.replace(" ", "")) - detail = chalobest.route[routes[0]] + routes = ChaloBest.routes(q=msg.text.replace(" ", "")) + detail = ChaloBest.route[routes[0]] stops = detail['stops']['features'] origin, destination = stops[0]['properties'], stops[-1]['properties'] msg.respond("%s: %s (%s) to %s (%s)" % (routes[0], origin['display_name'], origin['area'], destination['display_name'], destination['area'])) else: - stops = chalobest.stops(q=msg.text)['features'] - response = "" + stops = ChaloBest.stops(q=msg.text)['features'] + response = STYLE["start"] for feature in stops: stop = feature['properties'] - match = "%s (%s): %s" % (stop['official_name'], stop['area'], stop['routes']) - if len(response) + len(match) + 1 < MAX_MSG_LEN: - if response: response += ";" + area = PUNCT.sub('', stop['area']) + match = "%s (%s): %s" % (stop['official_name'], area, stop['routes']) + if len(response) + len(match) + len(STYLE["repeat"]) < MAX_MSG_LEN: + if len(response) > len(STYLE["repeat"]): response += SECTION_BREAK response += match else: break + response += STYLE["end"] msg.respond(response) From def02ca104b557c964144570187fc0bd4fef325b Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 12:15:32 -0800 Subject: [PATCH 16/25] shape up smsBEST parsing a bit. --- smsBEST/mumbai/app.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index 32c9ef2..c8d823a 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -6,9 +6,9 @@ MAX_MSG_LEN = 160 DIGIT = re.compile(r"\d{1,3}") PUNCT = re.compile(r"[^\w\s]") STYLE = { - "start": "-* ", - "repeat": "-*-", - "end": " *-" + "start": "-* ", + "repeat": " -*- ", + "end": " *-" } @@ -20,20 +20,29 @@ class App(AppBase): routes = ChaloBest.routes(q=msg.text.replace(" ", "")) detail = ChaloBest.route[routes[0]] stops = detail['stops']['features'] + if not stops: + msg.respond("Sorry, we found no route marked '%s'." % msg.text) + return origin, destination = stops[0]['properties'], stops[-1]['properties'] msg.respond("%s: %s (%s) to %s (%s)" % (routes[0], origin['display_name'], origin['area'], destination['display_name'], destination['area'])) else: stops = ChaloBest.stops(q=msg.text)['features'] + if not stops: + msg.respond("Sorry, we found no stops like '%s'." % msg.text) + return response = STYLE["start"] for feature in stops: stop = feature['properties'] area = PUNCT.sub('', stop['area']) match = "%s (%s): %s" % (stop['official_name'], area, stop['routes']) if len(response) + len(match) + len(STYLE["repeat"]) < MAX_MSG_LEN: - if len(response) > len(STYLE["repeat"]): response += SECTION_BREAK + if len(response) > len(STYLE["repeat"]): response += STYLE["repeat"] response += match + elif len(response) < len(STYLE["repeat"]): + response += match[:MAX_MSG_LEN-len(STYLE["start"])-len(STYLE["end"])+4] + response += " ..." else: break response += STYLE["end"] From 6f3136405d948a394b241c342c8c71d7c9a935c2 Mon Sep 17 00:00:00 2001 From: Sanj Date: Wed, 29 Feb 2012 01:49:03 +0530 Subject: [PATCH 17/25] route alias filter for checking exact number --- chaloBEST/mumbai/apiviews.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/chaloBEST/mumbai/apiviews.py b/chaloBEST/mumbai/apiviews.py index 0106e04..55370a7 100644 --- a/chaloBEST/mumbai/apiviews.py +++ b/chaloBEST/mumbai/apiviews.py @@ -3,6 +3,7 @@ from ox.django.shortcuts import get_object_or_404_json, render_to_json_response from django.contrib.auth.decorators import login_required import json from django.views.decorators.csrf import csrf_exempt +import re def route(request, slug): srid = int(request.GET.get("srid", 4326)) @@ -29,12 +30,26 @@ def area(request, slug): }) def routes(request): + q = request.GET.get("q", "") - if q != '': + in_regex = re.compile(r'(\d{1,3})') + match = re.findall(route_regex, q) + if match: + route_no = match[0] + else: + route_no = '' + ret = [] + if route_no != '': + out_regex = re.compile(r'.*(\D|\A)%s(\D|\Z).*' % route_no) qset = Route.objects.filter(alias__icontains=q) + for route in qset: + if re.match(out_regex, route.alias): + ret.append(route.alias) else: qset = Route.objects.all() - routes = [route.alias for route in qset] + for route in qset: + ret.append(route.alias) +# routes = [route.alias for route in qset] return render_to_json_response(routes) From b755c57e63ada49a7f055b02af4b6b95926f1efc Mon Sep 17 00:00:00 2001 From: Sanj Date: Wed, 29 Feb 2012 01:50:30 +0530 Subject: [PATCH 18/25] silly - typos --- chaloBEST/mumbai/apiviews.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chaloBEST/mumbai/apiviews.py b/chaloBEST/mumbai/apiviews.py index 55370a7..b0b4d54 100644 --- a/chaloBEST/mumbai/apiviews.py +++ b/chaloBEST/mumbai/apiviews.py @@ -33,7 +33,7 @@ def routes(request): q = request.GET.get("q", "") in_regex = re.compile(r'(\d{1,3})') - match = re.findall(route_regex, q) + match = re.findall(in_regex, q) if match: route_no = match[0] else: @@ -50,7 +50,7 @@ def routes(request): for route in qset: ret.append(route.alias) # routes = [route.alias for route in qset] - return render_to_json_response(routes) + return render_to_json_response(ret) def areas(request): From 9cf0a58c49673d569e8102a93e75b37f584b85db Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 12:31:09 -0800 Subject: [PATCH 19/25] shape up smsBEST parsing, add route grouping --- smsBEST/mumbai/app.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index c8d823a..637bdb9 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -24,7 +24,8 @@ class App(AppBase): msg.respond("Sorry, we found no route marked '%s'." % msg.text) return origin, destination = stops[0]['properties'], stops[-1]['properties'] - msg.respond("%s: %s (%s) to %s (%s)" % (routes[0], + msg.respond("%s: %s (%s) to %s (%s)" % ( + ",".join(routes), origin['display_name'], origin['area'], destination['display_name'], destination['area'])) else: @@ -37,13 +38,10 @@ class App(AppBase): stop = feature['properties'] area = PUNCT.sub('', stop['area']) match = "%s (%s): %s" % (stop['official_name'], area, stop['routes']) - if len(response) + len(match) + len(STYLE["repeat"]) < MAX_MSG_LEN: - if len(response) > len(STYLE["repeat"]): response += STYLE["repeat"] - response += match - elif len(response) < len(STYLE["repeat"]): - response += match[:MAX_MSG_LEN-len(STYLE["start"])-len(STYLE["end"])+4] - response += " ..." - else: - break + if len(response) > len(STYLE["repeat"]): response += STYLE["repeat"] + response += match + if len(response) > MAX_MSG_LEN: break + if len(response) > MAX_MSG_LEN: + response = response[:MAX_MSG_LEN-(len(STYLE["end"])+4)] + "..." response += STYLE["end"] msg.respond(response) From 219e703764e636ba5fd2bb18d9940fbbd59e0f4b Mon Sep 17 00:00:00 2001 From: Sanj Date: Wed, 29 Feb 2012 02:08:24 +0530 Subject: [PATCH 20/25] add comments for the route_no regexes --- chaloBEST/mumbai/apiviews.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chaloBEST/mumbai/apiviews.py b/chaloBEST/mumbai/apiviews.py index b0b4d54..c7afcbf 100644 --- a/chaloBEST/mumbai/apiviews.py +++ b/chaloBEST/mumbai/apiviews.py @@ -32,7 +32,7 @@ def area(request, slug): def routes(request): q = request.GET.get("q", "") - in_regex = re.compile(r'(\d{1,3})') + in_regex = re.compile(r'(\d{1,3})') # used to extract the route number string out of the query string - for eg, gets "21" from "21Ltd" match = re.findall(in_regex, q) if match: route_no = match[0] @@ -40,7 +40,7 @@ def routes(request): route_no = '' ret = [] if route_no != '': - out_regex = re.compile(r'.*(\D|\A)%s(\D|\Z).*' % route_no) + out_regex = re.compile(r'.*(\D|\A)%s(\D|\Z).*' % route_no) # used for, for eg. to filter out '210Ltd' when user searches for '21'. Checks for non-digit or start of string, followed by route_no, followed by non-digit or end of string qset = Route.objects.filter(alias__icontains=q) for route in qset: if re.match(out_regex, route.alias): From 50c702e381017b568acd0c58206a88c901572855 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 12:40:48 -0800 Subject: [PATCH 21/25] change style to semicolons; take areas out of name search --- smsBEST/mumbai/app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index 637bdb9..95cc4f0 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -5,11 +5,15 @@ import arrest MAX_MSG_LEN = 160 DIGIT = re.compile(r"\d{1,3}") PUNCT = re.compile(r"[^\w\s]") +""" STYLE = { "start": "-* ", "repeat": " -*- ", "end": " *-" } +""" + +STYLE = {"start": "", "repeat": "; ", "end": ""} ChaloBest = arrest.Client("http://chalobest.in/1.0") @@ -23,11 +27,11 @@ class App(AppBase): if not stops: msg.respond("Sorry, we found no route marked '%s'." % msg.text) return - origin, destination = stops[0]['properties'], stops[-1]['properties'] + origin, dest = stops[0]['properties'], stops[-1]['properties'] + origin_name, dest_name = origin['display_name'], dest['display_name'] + origin_area, dest_area = PUNCT.sub('', origin['area']), PUNCT.sub('', dest['area']) msg.respond("%s: %s (%s) to %s (%s)" % ( - ",".join(routes), - origin['display_name'], origin['area'], - destination['display_name'], destination['area'])) + ",".join(routes), origin_name, origin_area, dest_name, dest_area)) else: stops = ChaloBest.stops(q=msg.text)['features'] if not stops: @@ -36,8 +40,7 @@ class App(AppBase): response = STYLE["start"] for feature in stops: stop = feature['properties'] - area = PUNCT.sub('', stop['area']) - match = "%s (%s): %s" % (stop['official_name'], area, stop['routes']) + match = "%s: %s" % (stop['official_name'], stop['routes']) if len(response) > len(STYLE["repeat"]): response += STYLE["repeat"] response += match if len(response) > MAX_MSG_LEN: break From 6b847ae566543440445ccd4b688c2d07c6db521d Mon Sep 17 00:00:00 2001 From: Sanj Date: Wed, 29 Feb 2012 02:17:38 +0530 Subject: [PATCH 22/25] order route by code --- chaloBEST/mumbai/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chaloBEST/mumbai/models.py b/chaloBEST/mumbai/models.py index bae603d..34945ad 100644 --- a/chaloBEST/mumbai/models.py +++ b/chaloBEST/mumbai/models.py @@ -204,7 +204,7 @@ class Stop(models.Model): class Route(models.Model): - code = models.TextField(max_length=255, unique=True) + code = models.TextField(max_length=255, unique=True) #FIXME: Why is this a TextField?? slug = models.SlugField(null=True) alias = models.TextField(max_length=255) from_stop_txt = models.TextField(max_length=500) @@ -214,6 +214,9 @@ class Route(models.Model): distance = models.DecimalField(max_digits=3, decimal_places=1) stages = models.IntegerField() + class Meta: + ordering = ['code'] + def get_absolute_url(self): return "/route/%s/" % self.alias From f43fdf5004cf4947a0e3d9bbf5932a010af97735 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 12:56:10 -0800 Subject: [PATCH 23/25] Clean up repeated names in name queries. --- smsBEST/mumbai/app.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index 95cc4f0..a7b067e 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -33,14 +33,20 @@ class App(AppBase): msg.respond("%s: %s (%s) to %s (%s)" % ( ",".join(routes), origin_name, origin_area, dest_name, dest_area)) else: - stops = ChaloBest.stops(q=msg.text)['features'] - if not stops: + features = ChaloBest.stops(q=msg.text)['features'] + if not features: msg.respond("Sorry, we found no stops like '%s'." % msg.text) return + stops = [] + for feat in features: + stop = feat['properties'] + if stops and stop["official_name"] == stops[-1]["official_name"]: + stops[-1]["routes"] += ", " + stop["routes"] + else: + stops.append(stop) response = STYLE["start"] - for feature in stops: - stop = feature['properties'] - match = "%s: %s" % (stop['official_name'], stop['routes']) + for stop in stops: + match = stop["official_name"] + ": " + stop["routes"] if len(response) > len(STYLE["repeat"]): response += STYLE["repeat"] response += match if len(response) > MAX_MSG_LEN: break From 194a08c11c25aa67b61913162467c5b4cc50a986 Mon Sep 17 00:00:00 2001 From: Schuyler Erle Date: Tue, 28 Feb 2012 13:08:53 -0800 Subject: [PATCH 24/25] Actually report missing routes. --- smsBEST/mumbai/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smsBEST/mumbai/app.py b/smsBEST/mumbai/app.py index a7b067e..b8c0bbd 100644 --- a/smsBEST/mumbai/app.py +++ b/smsBEST/mumbai/app.py @@ -22,11 +22,11 @@ class App(AppBase): def handle(self, msg): if DIGIT.search(msg.text): routes = ChaloBest.routes(q=msg.text.replace(" ", "")) - detail = ChaloBest.route[routes[0]] - stops = detail['stops']['features'] - if not stops: + if not routes: msg.respond("Sorry, we found no route marked '%s'." % msg.text) return + detail = ChaloBest.route[routes[0]] + stops = detail['stops']['features'] origin, dest = stops[0]['properties'], stops[-1]['properties'] origin_name, dest_name = origin['display_name'], dest['display_name'] origin_area, dest_area = PUNCT.sub('', origin['area']), PUNCT.sub('', dest['area']) From dd3e59e7df97cc8ccdf52a260bbc9a7600668c89 Mon Sep 17 00:00:00 2001 From: Sanj Date: Wed, 29 Feb 2012 02:42:37 +0530 Subject: [PATCH 25/25] argh, silly error in routes apiview --- chaloBEST/mumbai/apiviews.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chaloBEST/mumbai/apiviews.py b/chaloBEST/mumbai/apiviews.py index c7afcbf..a461255 100644 --- a/chaloBEST/mumbai/apiviews.py +++ b/chaloBEST/mumbai/apiviews.py @@ -29,8 +29,7 @@ def area(request, slug): } }) -def routes(request): - +def routes(request): q = request.GET.get("q", "") in_regex = re.compile(r'(\d{1,3})') # used to extract the route number string out of the query string - for eg, gets "21" from "21Ltd" match = re.findall(in_regex, q) @@ -41,7 +40,7 @@ def routes(request): ret = [] if route_no != '': out_regex = re.compile(r'.*(\D|\A)%s(\D|\Z).*' % route_no) # used for, for eg. to filter out '210Ltd' when user searches for '21'. Checks for non-digit or start of string, followed by route_no, followed by non-digit or end of string - qset = Route.objects.filter(alias__icontains=q) + qset = Route.objects.filter(alias__icontains=route_no) for route in qset: if re.match(out_regex, route.alias): ret.append(route.alias) @@ -62,6 +61,7 @@ def areas(request): areas = [area.slug for area in qset] return render_to_json_response(areas) + def stops(request): q = request.GET.get("q", "") if q != '':