├── .gitignore ├── .gitmodules ├── CHANGES.txt ├── MANIFEST.in ├── OSMTM ├── __init__.py ├── history_meta.py ├── models.py ├── resources.py ├── static │ ├── crossdomain.xml │ ├── css │ │ ├── main.less │ │ ├── main.less.min.css │ │ ├── map.less │ │ └── reset.css │ ├── img │ │ ├── ajax-loader.gif │ │ ├── blank.gif │ │ ├── bm.jpg │ │ ├── closed_back.gif │ │ ├── feedback.png │ │ ├── glyphicons-halflings-white.png │ │ ├── glyphicons-halflings.png │ │ ├── lock.gif │ │ ├── map_pin.png │ │ ├── marker.png │ │ ├── marker.svg │ │ ├── marker_grey.png │ │ ├── marker_shadow.png │ │ ├── ol_labeled.svg │ │ ├── split.png │ │ ├── status-offline.png │ │ ├── status.png │ │ ├── tour │ │ │ ├── tour1.png │ │ │ ├── tour3.png │ │ │ ├── tour_auth_osm.png │ │ │ ├── tour_home.png │ │ │ ├── tour_job_task_do.png │ │ │ ├── tour_job_task_take.png │ │ │ ├── tour_job_workflow.png │ │ │ └── tour_josm.png │ │ ├── world_map.png │ │ └── zoom-panel.png │ ├── js │ │ ├── home.js │ │ ├── job.edit.js │ │ ├── job.js │ │ ├── job.new.js │ │ ├── less-1.2.2.min.js │ │ ├── lib │ │ │ ├── OpenLayers.js │ │ │ ├── highcharts.js │ │ │ ├── jquery-1.7.1.min.js │ │ │ ├── knockout-2.1.0.js │ │ │ ├── sammy-latest.min.js │ │ │ └── showdown.js │ │ ├── main.js │ │ ├── ol_osmtm.cfg │ │ └── task.js │ ├── middlebg.png │ ├── red-header.png │ ├── theme │ │ └── default │ │ │ └── style.css │ ├── thumb-up.png │ └── thumb.png ├── templates │ ├── about.mako │ ├── admin.mako │ ├── base.mako │ ├── forbidden.mako │ ├── home.mako │ ├── imagery.mako │ ├── job.edit.mako │ ├── job.mako │ ├── job.new.mako │ ├── job.tags.mako │ ├── job.task_extra.mako │ ├── job.users.mako │ ├── license.edit.mako │ ├── license.mako │ ├── licenses.mako │ ├── task.comments.mako │ ├── task.empty.mako │ ├── task.gpx.mako │ ├── task.mako │ ├── task.osm.mako │ ├── tour.mako │ ├── user.mako │ ├── user_edit.mako │ └── users.mako ├── tests.py ├── utils.py └── views │ ├── __init__.py │ ├── admin.py │ ├── crossdomain.py │ ├── jobs.py │ ├── license.py │ ├── osmproxy.py │ ├── security.py │ ├── tasks.py │ └── views.py ├── README.rst ├── README.txt ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ ├── 173a7003596f_added_author_column.py │ ├── 183cfaae4493_adding_done_and_upda.py │ ├── 2edcc95ff6f7_.py │ ├── 2faee55a9ed8_upgrade_after_workfl.py │ ├── 41cd7451a8d7_adding_offset_fields.py │ ├── 4fbba110ab8b_adding_index_for_til.py │ ├── 5229d2fd908d_adding_task_extra_co.py │ └── 55009f9d3296_added_licenses_table.py ├── development.ini ├── migration ├── README ├── __init__.py ├── manage.py ├── migrate.cfg └── versions │ ├── 001_Adding_next_view_related_table_and_columns.py │ ├── 002_Remove_role_column,_add_admin_column.py │ ├── 003_Add_short_description_column.py │ ├── 004_Change_checkout_column_to_update.py │ ├── 005_Add_status_column_in_jobs_table.py │ ├── 006_Add_tags.py │ ├── 007_Add_preset_column.py │ ├── 008_Adding_new_change_column.py │ ├── 009_Adding_featured_attribute.py │ ├── 010_Move_zoom_from_job_to_task.py │ ├── __init__.py │ ├── data_upgrade_008.py │ └── data_upgrade_010.py ├── production.ini ├── scripts ├── delete.py └── update.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | OSMTM.egg-info/ 2 | data/ 3 | *.pyc 4 | env/ 5 | build/ 6 | dist/ 7 | *.db 8 | *.sw* 9 | .coverage 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "OSMTM/static/js/openlayers"] 2 | path = OSMTM/static/js/openlayers 3 | url = git://github.com/openlayers/openlayers.git 4 | [submodule "OSMTM/static/bootstrap"] 5 | path = OSMTM/static/bootstrap 6 | url = git://github.com/twbs/bootstrap.git 7 | [submodule "OSMTM/static/js/lib/openlayers"] 8 | path = OSMTM/static/js/lib/openlayers 9 | url = git://github.com/openlayers/openlayers.git 10 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 0.2.1 (2014-02-09) 2 | ------------------ 3 | 4 | - Fixing minor bugs (link to user page, per user map) 5 | 6 | 0.2 (2014-02-09) 7 | ---------------- 8 | 9 | - Major changes in the UI 10 | 11 | - Task modifications history 12 | 13 | - Deep linking 14 | 15 | - Better iD integration (gpx and imagery auto loads) 16 | 17 | - Fixed python packages dependency (simplejson) 18 | 19 | 0.1 (2014-02-09) 20 | ---------------- 21 | 22 | - Before workflow branch merge 23 | 24 | 0.0 25 | --- 26 | 27 | - Initial version 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.ini *.cfg *.rst 2 | recursive-include OSMTM *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml 3 | -------------------------------------------------------------------------------- /OSMTM/__init__.py: -------------------------------------------------------------------------------- 1 | from pyramid_beaker import session_factory_from_settings 2 | from pyramid.config import Configurator 3 | from sqlalchemy import engine_from_config 4 | from pyramid.authentication import AuthTktAuthenticationPolicy 5 | from pyramid.authorization import ACLAuthorizationPolicy 6 | from papyrus.renderers import GeoJSON 7 | 8 | from OSMTM.models import initialize_sql, group_membership 9 | 10 | def main(global_config, **settings): 11 | """ This function returns a Pyramid WSGI application. 12 | """ 13 | 14 | settings['mako.directories'] = 'OSMTM:templates' 15 | engine = engine_from_config(settings, 'sqlalchemy.') 16 | admin_user = settings['admin_user'] 17 | initialize_sql(engine, admin_user) 18 | authn_policy = AuthTktAuthenticationPolicy( 19 | secret='super_secret', callback=group_membership) 20 | authz_policy = ACLAuthorizationPolicy() 21 | config = Configurator(settings=settings, 22 | root_factory='OSMTM.models.RootFactory', 23 | authentication_policy=authn_policy, 24 | authorization_policy=authz_policy) 25 | 26 | session_factory = session_factory_from_settings(settings) 27 | config.set_session_factory(session_factory) 28 | 29 | config.add_static_view('static', 'OSMTM:static', cache_max_age=3600) 30 | 31 | config.add_view('OSMTM.views.crossdomain.crossdomain_view', name='crossdomain.xml') 32 | config.add_route('crossdomain', '/crossdomain.xml', 33 | view='OSMTM.views.crossdomain.crossdomain_view') 34 | config.add_route('home', '/') 35 | config.add_route('about', '/about') 36 | config.add_route('tour', '/tour') 37 | config.add_route('login', '/login') 38 | config.add_route('logout', '/logout') 39 | config.add_route('admin', '/admin') 40 | config.add_route('job_new', '/job/new') 41 | config.add_route('job_geom', '/job/{job}.json') 42 | config.add_route('job_tiles', '/job/{job}/tiles') 43 | config.add_route('job_tiles_status', '/job/{job}/tiles_status') 44 | config.add_route('job', '/job/{job}', factory='OSMTM.resources.JobFactory') 45 | config.add_route('job_edit', '/job/{job}/edit', factory='OSMTM.resources.JobFactory') 46 | config.add_route('job_feature', '/job/{job}/feature', factory='OSMTM.resources.JobFactory') 47 | config.add_route('job_archive', '/job/{job}/archive', factory='OSMTM.resources.JobFactory') 48 | config.add_route('job_publish', '/job/{job}/publish', factory='OSMTM.resources.JobFactory') 49 | config.add_route('job_users', '/job/{job}/users', factory='OSMTM.resources.JobFactory') 50 | config.add_route('job_user', '/job/{job}/user/{user}', factory='OSMTM.resources.JobFactory') 51 | config.add_route('job_tags', '/job/{job}/tags', factory='OSMTM.resources.JobFactory') 52 | config.add_route('job_export', '/job/{job}/export', factory='OSMTM.resources.JobFactory') 53 | config.add_route('job_preset', '/job/{job}/preset', factory='OSMTM.resources.JobFactory') 54 | config.add_route('job_stats', '/job/{job}/stats', factory='OSMTM.resources.JobFactory') 55 | config.add_route('job_contributors', '/job/{job}/contributors', factory='OSMTM.resources.JobFactory') 56 | config.add_route('task_take_random', '/job/{job}/task/take/{checkin}', factory='OSMTM.resources.JobFactory') 57 | config.add_route('task_xhr', '/job/{job}/task/{x}/{y}/{zoom}', factory='OSMTM.resources.JobFactory', xhr=True) 58 | config.add_route('task_empty', '/job/{job}/task', factory='OSMTM.resources.JobFactory') 59 | config.add_route('task', '/job/{job}/task/{x}/{y}/{zoom}', factory='OSMTM.resources.JobFactory') 60 | config.add_route('task_unlock', '/job/{job}/task/{x}/{y}/{zoom}/unlock', factory='OSMTM.resources.JobFactory') 61 | config.add_route('task_done', '/job/{job}/task/{x}/{y}/{zoom}/done', factory='OSMTM.resources.JobFactory') 62 | config.add_route('task_lock', '/job/{job}/task/{x}/{y}/{zoom}/lock', factory='OSMTM.resources.JobFactory') 63 | config.add_route('task_export_osm', '/job/{job}/task/{x}/{y}/{zoom}/export.osm', factory='OSMTM.resources.JobFactory') 64 | config.add_route('task_export_gpx', '/job/{job}/task/{x}/{y}/{zoom}/export.gpx', factory='OSMTM.resources.JobFactory') 65 | config.add_route('task_split', '/job/{job}/task/{x}/{y}/{zoom}/split', factory='OSMTM.resources.JobFactory') 66 | config.add_route('license_new', '/license/new') 67 | config.add_route('license', '/license/{license}') 68 | config.add_route('licenses', '/licenses') 69 | config.add_route('license_edit', '/license/{license}/edit') 70 | config.add_route('license_delete', '/license/{license}/delete') 71 | config.add_route('user_add', '/user/add') 72 | config.add_route('user', '/user/{id}') 73 | config.add_route('user_edit', '/user/{id}/edit') 74 | config.add_route('user_update', '/user/{id}/update') 75 | config.add_route('users', '/users') 76 | config.add_route('osmproxy', '/osmproxy') 77 | config.add_route('oauth_callback', '/oauth_callback') 78 | config.add_view('OSMTM.views.security.login', 79 | renderer='forbidden.mako', 80 | context='pyramid.exceptions.Forbidden') 81 | 82 | config.add_renderer('geojson', GeoJSON()) 83 | 84 | config.scan() 85 | return config.make_wsgi_app() 86 | 87 | -------------------------------------------------------------------------------- /OSMTM/history_meta.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import DeclarativeMeta 2 | from sqlalchemy.orm import mapper, class_mapper, attributes, object_mapper 3 | from sqlalchemy.orm.exc import UnmappedClassError, UnmappedColumnError 4 | from sqlalchemy import Table, Column, ForeignKeyConstraint, Integer 5 | from sqlalchemy.orm.interfaces import SessionExtension 6 | from sqlalchemy.orm.properties import RelationshipProperty 7 | 8 | def col_references_table(col, table): 9 | for fk in col.foreign_keys: 10 | if fk.references(table): 11 | return True 12 | return False 13 | 14 | def _history_mapper(local_mapper): 15 | cls = local_mapper.class_ 16 | 17 | # set the "active_history" flag 18 | # on on column-mapped attributes so that the old version 19 | # of the info is always loaded (currently sets it on all attributes) 20 | for prop in local_mapper.iterate_properties: 21 | getattr(local_mapper.class_, prop.key).impl.active_history = True 22 | 23 | super_mapper = local_mapper.inherits 24 | super_history_mapper = getattr(cls, '__history_mapper__', None) 25 | 26 | polymorphic_on = None 27 | super_fks = [] 28 | if not super_mapper or local_mapper.local_table is not super_mapper.local_table: 29 | cols = [] 30 | for column in local_mapper.local_table.c: 31 | if column.name == 'version': 32 | continue 33 | 34 | col = column.copy() 35 | col.unique = False 36 | 37 | if super_mapper and col_references_table(column, super_mapper.local_table): 38 | super_fks.append((col.key, list(super_history_mapper.base_mapper.local_table.primary_key)[0])) 39 | 40 | cols.append(col) 41 | 42 | if column is local_mapper.polymorphic_on: 43 | polymorphic_on = col 44 | 45 | if super_mapper: 46 | super_fks.append(('version', super_history_mapper.base_mapper.local_table.c.version)) 47 | cols.append(Column('version', Integer, primary_key=True)) 48 | else: 49 | cols.append(Column('version', Integer, primary_key=True)) 50 | 51 | if super_fks: 52 | cols.append(ForeignKeyConstraint(*zip(*super_fks))) 53 | 54 | table = Table(local_mapper.local_table.name + '_history', local_mapper.local_table.metadata, 55 | *cols 56 | ) 57 | else: 58 | # single table inheritance. take any additional columns that may have 59 | # been added and add them to the history table. 60 | for column in local_mapper.local_table.c: 61 | if column.key not in super_history_mapper.local_table.c: 62 | col = column.copy() 63 | super_history_mapper.local_table.append_column(col) 64 | table = None 65 | 66 | if super_history_mapper: 67 | bases = (super_history_mapper.class_,) 68 | else: 69 | bases = local_mapper.base_mapper.class_.__bases__ 70 | versioned_cls = type.__new__(type, "%sHistory" % cls.__name__, bases, {}) 71 | 72 | m = mapper( 73 | versioned_cls, 74 | table, 75 | inherits=super_history_mapper, 76 | polymorphic_on=polymorphic_on, 77 | polymorphic_identity=local_mapper.polymorphic_identity 78 | ) 79 | cls.__history_mapper__ = m 80 | 81 | if not super_history_mapper: 82 | cls.version = Column('version', Integer, default=1, nullable=False) 83 | 84 | 85 | class VersionedMeta(DeclarativeMeta): 86 | def __init__(cls, classname, bases, dict_): 87 | DeclarativeMeta.__init__(cls, classname, bases, dict_) 88 | 89 | try: 90 | mapper = class_mapper(cls) 91 | _history_mapper(mapper) 92 | except UnmappedClassError: 93 | pass 94 | 95 | 96 | def versioned_objects(iter): 97 | for obj in iter: 98 | if hasattr(obj, '__history_mapper__'): 99 | yield obj 100 | 101 | def create_version(obj, session, deleted = False): 102 | obj_mapper = object_mapper(obj) 103 | history_mapper = obj.__history_mapper__ 104 | history_cls = history_mapper.class_ 105 | 106 | obj_state = attributes.instance_state(obj) 107 | 108 | attr = {} 109 | 110 | obj_changed = False 111 | 112 | for om, hm in zip(obj_mapper.iterate_to_root(), history_mapper.iterate_to_root()): 113 | if hm.single: 114 | continue 115 | 116 | for hist_col in hm.local_table.c: 117 | if hist_col.key == 'version': 118 | continue 119 | 120 | obj_col = om.local_table.c[hist_col.key] 121 | 122 | # get the value of the 123 | # attribute based on the MapperProperty related to the 124 | # mapped column. this will allow usage of MapperProperties 125 | # that have a different keyname than that of the mapped column. 126 | try: 127 | prop = obj_mapper.get_property_by_column(obj_col) 128 | except UnmappedColumnError: 129 | # in the case of single table inheritance, there may be 130 | # columns on the mapped table intended for the subclass only. 131 | # the "unmapped" status of the subclass column on the 132 | # base class is a feature of the declarative module as of sqla 0.5.2. 133 | continue 134 | 135 | # expired object attributes and also deferred cols might not be in the 136 | # dict. force it to load no matter what by using getattr(). 137 | if prop.key not in obj_state.dict: 138 | getattr(obj, prop.key) 139 | 140 | a, u, d = attributes.get_history(obj, prop.key) 141 | 142 | if d: 143 | attr[hist_col.key] = d[0] 144 | obj_changed = True 145 | elif u: 146 | attr[hist_col.key] = u[0] 147 | else: 148 | # if the attribute had no value. 149 | attr[hist_col.key] = a[0] 150 | obj_changed = True 151 | 152 | if not obj_changed: 153 | # not changed, but we have relationships. OK 154 | # check those too 155 | for prop in obj_mapper.iterate_properties: 156 | if isinstance(prop, RelationshipProperty) and \ 157 | attributes.get_history(obj, prop.key).has_changes(): 158 | obj_changed = True 159 | break 160 | 161 | if not obj_changed and not deleted: 162 | return 163 | 164 | attr['version'] = obj.version 165 | hist = history_cls() 166 | for key, value in attr.iteritems(): 167 | setattr(hist, key, value) 168 | session.add(hist) 169 | obj.version += 1 170 | 171 | class VersionedListener(SessionExtension): 172 | def before_flush(self, session, flush_context, instances): 173 | for obj in versioned_objects(session.dirty): 174 | create_version(obj, session) 175 | for obj in versioned_objects(session.deleted): 176 | create_version(obj, session, deleted = True) 177 | -------------------------------------------------------------------------------- /OSMTM/resources.py: -------------------------------------------------------------------------------- 1 | from pyramid.httpexceptions import HTTPFound 2 | from pyramid.url import route_url 3 | from pyramid.security import Allow, Deny, Everyone 4 | from models import Job, User, RootFactory, DBSession 5 | 6 | 7 | class JobFactory(RootFactory): 8 | def __init__(self, request): 9 | session = DBSession() 10 | job_id = request.matchdict['job'] 11 | job = session.query(Job).get(job_id) 12 | if job is not None and job.is_private: 13 | acl = [ 14 | (Allow, 'job:'+job_id, 'job'), 15 | (Allow, 'group:admin', 'job'), 16 | (Deny, Everyone, 'job'), 17 | ] 18 | self.__acl__ = acl + list(self.__acl__) 19 | -------------------------------------------------------------------------------- /OSMTM/static/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /OSMTM/static/css/map.less: -------------------------------------------------------------------------------- 1 | .olTile { 2 | /*margin: 4px;*/ 3 | border: 1px solid grey; 4 | } 5 | .olTile div { 6 | height: 100%; 7 | width: 100%; 8 | position: absolute; 9 | } 10 | .olTile div.back { 11 | opacity: 0.4; 12 | } 13 | .hasTags div.back { 14 | background-color: grey; 15 | } 16 | .hover div.back { 17 | background-color: grey; 18 | opacity: 0.6; 19 | } 20 | .olTileImage { 21 | /* override bootstrap */ 22 | max-width: none; 23 | }u 24 | lTile { 25 | /*margin: 4px;*/ 26 | border: 1px solid grey; 27 | } 28 | .olTile div { 29 | height: 100%; 30 | width: 100%; 31 | position: absolute; 32 | } 33 | .olTile div.back { 34 | opacity: 0.4; 35 | } 36 | .hasTags div.back { 37 | background-color: grey; 38 | } 39 | .hover div.back { 40 | background-color: grey; 41 | opacity: 0.6; 42 | } 43 | .olTileImage { 44 | /* override bootstrap */ 45 | max-width: none; 46 | } 47 | -------------------------------------------------------------------------------- /OSMTM/static/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ */ 2 | /* v1.0 | 20080212 */ 3 | 4 | html, body, div, span, applet, object, iframe, 5 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 6 | a, abbr, acronym, address, big, cite, code, 7 | del, dfn, em, font, img, ins, kbd, q, s, samp, 8 | small, strike, strong, sub, sup, tt, var, 9 | b, u, i, center, 10 | dl, dt, dd, ol, ul, li, 11 | fieldset, form, label, legend, 12 | table, caption, tbody, tfoot, thead, tr, th, td { 13 | margin: 0; 14 | padding: 0; 15 | border: 0; 16 | outline: 0; 17 | font-size: 100%; 18 | vertical-align: baseline; 19 | background: transparent; 20 | } 21 | body { 22 | line-height: 1; 23 | } 24 | ol, ul { 25 | list-style: none; 26 | } 27 | blockquote, q { 28 | quotes: none; 29 | } 30 | blockquote:before, blockquote:after, 31 | q:before, q:after { 32 | content: ''; 33 | content: none; 34 | } 35 | 36 | /* remember to define focus styles! */ 37 | :focus { 38 | outline: 0; 39 | } 40 | 41 | /* remember to highlight inserts somehow! */ 42 | ins { 43 | text-decoration: none; 44 | } 45 | del { 46 | text-decoration: line-through; 47 | } 48 | 49 | /* tables still need 'cellspacing="0"' in the markup */ 50 | table { 51 | border-collapse: collapse; 52 | border-spacing: 0; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /OSMTM/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /OSMTM/static/img/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/blank.gif -------------------------------------------------------------------------------- /OSMTM/static/img/bm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/bm.jpg -------------------------------------------------------------------------------- /OSMTM/static/img/closed_back.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/closed_back.gif -------------------------------------------------------------------------------- /OSMTM/static/img/feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/feedback.png -------------------------------------------------------------------------------- /OSMTM/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /OSMTM/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /OSMTM/static/img/lock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/lock.gif -------------------------------------------------------------------------------- /OSMTM/static/img/map_pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/map_pin.png -------------------------------------------------------------------------------- /OSMTM/static/img/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/marker.png -------------------------------------------------------------------------------- /OSMTM/static/img/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 27 | 34 | 40 | 44 | 48 | 54 | 58 | 59 | 66 | 70 | 71 | 78 | 85 | 93 | 97 | 98 | 99 | 118 | 121 | 122 | 124 | 125 | 127 | image/svg+xml 128 | 130 | 131 | 132 | 133 | 134 | 138 | 144 | 150 | 157 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /OSMTM/static/img/marker_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/marker_grey.png -------------------------------------------------------------------------------- /OSMTM/static/img/marker_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/marker_shadow.png -------------------------------------------------------------------------------- /OSMTM/static/img/split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/split.png -------------------------------------------------------------------------------- /OSMTM/static/img/status-offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/status-offline.png -------------------------------------------------------------------------------- /OSMTM/static/img/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/status.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour1.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour3.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour_auth_osm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour_auth_osm.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour_home.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour_job_task_do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour_job_task_do.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour_job_task_take.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour_job_task_take.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour_job_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour_job_workflow.png -------------------------------------------------------------------------------- /OSMTM/static/img/tour/tour_josm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/tour/tour_josm.png -------------------------------------------------------------------------------- /OSMTM/static/img/world_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/world_map.png -------------------------------------------------------------------------------- /OSMTM/static/img/zoom-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/img/zoom-panel.png -------------------------------------------------------------------------------- /OSMTM/static/js/home.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('.delete') 3 | .click(function() { 4 | if (!confirm("Are you sure you want to delete this job?")) { 5 | return false; 6 | } 7 | }); 8 | 9 | ko.applyBindings(new JobViewModel(jobs)); 10 | }); 11 | /** 12 | * Knockout js model 13 | */ 14 | function JobViewModel(initialJobs) { 15 | // Data 16 | var self = this; 17 | self.jobs = ko.observableArray(); 18 | self.filter = ko.observable('featured'); 19 | self.searchValue = ko.observable(); 20 | 21 | function changeHash() { 22 | var search = this.searchValue() ? 23 | '/' + this.searchValue() : ''; 24 | location.hash = this.filter() + search; 25 | } 26 | self.searchValue.subscribe(changeHash, this); 27 | self.filter.subscribe(changeHash, this); 28 | self.jobs(initialJobs); 29 | 30 | this.filterByFeatured = function() { 31 | var jobs = this.jobs(); 32 | return ko.utils.arrayFirst(jobs, function(job) { 33 | return job.featured === true; 34 | }); 35 | }.bind(this); 36 | 37 | this.showFeatured = function() { 38 | this.filter('featured'); 39 | }.bind(this); 40 | this.showMine = function() { 41 | this.filter('mine'); 42 | }.bind(this); 43 | 44 | this.search = function() { 45 | var jobs = [].concat(initialJobs); 46 | this.jobs(jobs); 47 | var self = this; 48 | var searchVal = this.searchValue() && this.searchValue().toLowerCase(); 49 | var filter = this.filter(); 50 | this.jobs.remove(function(job) { 51 | var text = [job.title, job.description, job.short_description, job.tags.join(',')].join(''); 52 | return (searchVal && text.toLowerCase().indexOf(searchVal) == -1) || 53 | (filter == 'featured' && job.featured !== true) || 54 | (filter == 'mine' && job.is_mine !== true); 55 | }); 56 | }.bind(this); 57 | 58 | this.clearFilter = function() { 59 | this.filter('all'); 60 | }.bind(this); 61 | 62 | // Client-side routes 63 | Sammy(function() { 64 | this.get('/', function() { 65 | self.filter('featured'); 66 | self.search(); 67 | }); 68 | this.get('#:filter', function() { 69 | self.filter(this.params.filter); 70 | self.search(); 71 | }); 72 | this.get('#:filter/:search', function() { 73 | self.filter(this.params.filter); 74 | self.searchValue(this.params.search); 75 | self.search(); 76 | }); 77 | }).run(); 78 | } 79 | -------------------------------------------------------------------------------- /OSMTM/static/js/job.edit.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var converter = new Showdown.converter(); 3 | 4 | var short_description = $('#id_short_description'), 5 | short_description_preview = $('#short_description_preview'); 6 | short_description.keyup(function() { 7 | var html = converter.makeHtml(short_description.val()); 8 | short_description_preview.html(html); 9 | }).trigger('keyup'); 10 | 11 | var description = $('#id_description'), 12 | description_preview = $('#description_preview'); 13 | description.keyup(function() { 14 | var html = converter.makeHtml(description.val()); 15 | description_preview.html(html); 16 | }).trigger('keyup'); 17 | 18 | var workflow = $('#id_workflow'), 19 | workflow_preview = $('#workflow_preview'); 20 | workflow.keyup(function() { 21 | var html = converter.makeHtml(workflow.val()); 22 | workflow_preview.html(html); 23 | }).trigger('keyup'); 24 | }); 25 | -------------------------------------------------------------------------------- /OSMTM/static/js/job.new.js: -------------------------------------------------------------------------------- 1 | var map = null, 2 | vectorLayer = null, 3 | tiles = null; 4 | 5 | function updateSubmitBtnStatus() { 6 | var disabled = $('#id_title').val() === '' || 7 | $('#geometry').val() === ''; 8 | $('#id_submit')[0].disabled = disabled; 9 | } 10 | 11 | function resetMap () { 12 | $('#geometry').val(''); 13 | updateSubmitBtnStatus(); 14 | $('#map').show(); 15 | map && map.destroy(); 16 | map = new OpenLayers.Map('map', { 17 | controls: [], 18 | theme: null 19 | }); 20 | var osm = new OpenLayers.Layer.OSM(); 21 | map.addLayer(osm); 22 | } 23 | 24 | function activateDrawControl() { 25 | $('#relation_loading_msg').hide(); 26 | vectorLayer = new OpenLayers.Layer.Vector("Job Area"); 27 | map.addLayer(vectorLayer); 28 | var control = new OpenLayers.Control.DrawFeature( 29 | vectorLayer, 30 | OpenLayers.Handler.Polygon, { 31 | "callbacks": { 32 | point: function(feature) { 33 | vectorLayer.destroyFeatures(); 34 | } 35 | } 36 | }); 37 | vectorLayer.events.on({ 38 | featureadded: function(obj) { 39 | var feature = obj.feature; 40 | var format = new OpenLayers.Format.WKT(); 41 | $('#geometry').val(format.write(feature)); 42 | adaptZoomLevel(vectorLayer.getDataExtent()); 43 | updateSubmitBtnStatus(); 44 | } 45 | }); 46 | map.addControls([ 47 | new OpenLayers.Control.ZoomPanel(), 48 | new OpenLayers.Control.Navigation(), 49 | control 50 | ]); 51 | control.activate(); 52 | if (!map.getCenter()) { 53 | map.zoomToMaxExtent(); 54 | } 55 | } 56 | 57 | // set the zoom level to the more appropriate value 58 | function adaptZoomLevel(bounds) { 59 | var zoom = $('#id_zoom').val(), 60 | res = map.getResolutionForZoom(zoom), 61 | // the size of a tile in meters for the given zoom 62 | tileSize = res * 256, 63 | // the number of tile in a row 64 | nbByRow = Math.abs(bounds.right - bounds.left) / tileSize, 65 | nbByCol = Math.abs(bounds.top - bounds.bottom) / tileSize; 66 | $('#zoom_level_info').show(); 67 | $('#nb_tiles').html(Math.round(nbByRow * nbByCol)); 68 | } 69 | 70 | $('input[name=relation_type]') 71 | .change(function() { 72 | resetMap(); 73 | if ($(this).val() == "relation") { 74 | $('#id_relation').attr('disabled', false); 75 | $('#id_relation').val(''); 76 | } else { 77 | $('#id_relation').attr('disabled', true); 78 | activateDrawControl(); 79 | } 80 | }); 81 | 82 | $('#id_title').focus(); 83 | 84 | $('#id_relation') 85 | .change(function() { 86 | $('#geometry').val(''); 87 | if ($("input[name=relation_type]").val() == "relation") { 88 | $('#relation_loading_msg').show(); 89 | var url = "http://www.openstreetmap.org/api/0.6/relation/" + this.value + '/full'; 90 | vectorLayer = new OpenLayers.Layer.Vector("Objects", { 91 | protocol: new OpenLayers.Protocol.HTTP({ 92 | url: url, 93 | format: new OpenLayers.Format.GeoJSON() 94 | }), 95 | strategies: [new OpenLayers.Strategy.Fixed()], 96 | style: { 97 | strokeColor: "blue", 98 | strokeWidth: 3, 99 | strokeOpacity: 0.5, 100 | fillOpacity: 0.2, 101 | fillColor: "lightblue", 102 | pointRadius: 6 103 | }, 104 | projection: new OpenLayers.Projection("EPSG:4326"), 105 | displayInLayerSwitcher: false 106 | }); 107 | 108 | vectorLayer.events.register("loadend", vectorLayer, function() { 109 | $('#relation_loading_msg').hide(); 110 | map.zoomToExtent(vectorLayer.getDataExtent()); 111 | var format = new OpenLayers.Format.WKT(); 112 | $('#geometry').val(format.write(vectorLayer.features[0])); 113 | adaptZoomLevel(vectorLayer.getDataExtent()); 114 | updateSubmitBtnStatus(); 115 | }); 116 | 117 | map.addLayer(vectorLayer); 118 | } 119 | }); 120 | 121 | $('#id_zoom') 122 | .change(function() { 123 | adaptZoomLevel(vectorLayer.getDataExtent()); 124 | }); 125 | 126 | $('#id_title') 127 | .change(function() { 128 | updateSubmitBtnStatus(); 129 | }); 130 | 131 | $(document).ready(function() { 132 | 133 | var converter = new Showdown.converter(), 134 | to_convert = ['#id_short_description', '#id_description', '#id_workflow']; 135 | 136 | $(to_convert).each(function(i, sel){ 137 | var textarea = $(sel), 138 | preview = $('
').appendTo(sel+'_preview'); 139 | 140 | textarea.keyup(function() { 141 | var html = converter.makeHtml(textarea.val()); 142 | preview.html(html); 143 | }).trigger('keyup'); 144 | }); 145 | 146 | resetMap(); 147 | }); 148 | -------------------------------------------------------------------------------- /OSMTM/static/js/main.js: -------------------------------------------------------------------------------- 1 | $().ready(function() { 2 | $('#flash').fadeIn().delay(2000).fadeOut(400); 3 | $("a[rel=popover]") 4 | .popover({ 5 | offset: 10, 6 | html: true 7 | }) 8 | .click(function(e) { 9 | e.preventDefault(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /OSMTM/static/js/ol_osmtm.cfg: -------------------------------------------------------------------------------- 1 | # This file includes a small subset of OpenLayers code, designed to be 2 | # integrated into another application. It includes only the Layer types 3 | # neccesary to create tiled or untiled WMS, and does not include any Controls. 4 | # This is the result of what was at the time called "Webmap.js" at the FOSS4G 5 | # Web Mapping BOF. 6 | 7 | [first] 8 | 9 | [last] 10 | 11 | [include] 12 | OpenLayers/Map.js 13 | OpenLayers/Layer/OSM.js 14 | OpenLayers/Layer/Image.js 15 | OpenLayers/Layer/SphericalMercator.js 16 | OpenLayers/Renderer/SVG.js 17 | OpenLayers/Renderer/Canvas.js 18 | OpenLayers/Layer/Vector.js 19 | OpenLayers/Format/GeoJSON.js 20 | OpenLayers/Format/WKT.js 21 | OpenLayers/Geometry/Polygon.js 22 | OpenLayers/Control/SelectFeature.js 23 | OpenLayers/Control/Navigation.js 24 | OpenLayers/Control/ZoomPanel.js 25 | OpenLayers/Control/Attribution.js 26 | OpenLayers/Protocol/HTTP.js 27 | OpenLayers/Strategy/Fixed.js 28 | OpenLayers/Control/DrawFeature.js 29 | OpenLayers/Handler/Polygon.js 30 | 31 | [exclude] 32 | 33 | 34 | -------------------------------------------------------------------------------- /OSMTM/static/js/task.js: -------------------------------------------------------------------------------- 1 | var roundd = function(input, decimals) { 2 | var p = Math.pow(10, decimals); 3 | return Math.round(input*p)/p; 4 | }; 5 | var getLink = function(options) { 6 | if (options.protocol === 'lbrt') { 7 | var bounds = options.bounds; 8 | return options.base + OpenLayers.Util.getParameterString({ 9 | left: roundd(bounds.left,5), 10 | bottom: roundd(bounds.bottom,5), 11 | right: roundd(bounds.right,5), 12 | top: roundd(bounds.top,5) 13 | }); 14 | } else if (options.protocol === 'llz') { 15 | var c = options.bounds.getCenterLonLat(); 16 | return options.base + OpenLayers.Util.getParameterString({ 17 | lon: roundd(c.lon,5), 18 | lat: roundd(c.lat,5), 19 | zoom: options.zoom || 15 20 | }); 21 | } else if (options.protocol === 'id') { 22 | var c = options.bounds.getCenterLonLat(); 23 | return options.base + '#map=' + [options.zoom, c.lat, c.lon].join('/'); 24 | } 25 | }; 26 | var exportOpen = function(evt) { 27 | 28 | // if the clicked link has 'disabled' class stop event processing 29 | if ($(evt.target).hasClass('disabled')) { 30 | return false; 31 | }; 32 | 33 | var url, 34 | format = new OpenLayers.Format.GeoJSON(), 35 | f = format.read(current_tile)[0], 36 | bounds = f.geometry.getBounds(); 37 | 38 | bounds.transform( 39 | new OpenLayers.Projection("EPSG:900913"), 40 | new OpenLayers.Projection("EPSG:4326") 41 | ); 42 | 43 | switch (this.id) { 44 | case "josm": 45 | url = getLink({ 46 | base: 'http://127.0.0.1:8111/load_and_zoom?', 47 | bounds: bounds, 48 | protocol: 'lbrt' 49 | }); 50 | $.ajax({ 51 | url: url, 52 | complete: function(t) { 53 | if (t.status != 200) { 54 | alert("JOSM remote control did not respond. Do you have JOSM running and configured to be controlled remotely?"); 55 | } 56 | } 57 | }); 58 | break; 59 | case "potlatch2": 60 | url = getLink({ 61 | base: 'http://www.openstreetmap.org/edit?editor=potlatch2&', 62 | bounds: bounds, 63 | zoom: zoom, 64 | protocol: 'llz' 65 | }); 66 | window.open(url); 67 | break; 68 | case "wp": 69 | url = getLink({ 70 | base: 'http://walking-papers.org/?', 71 | bounds: bounds, 72 | protocol: 'llz' 73 | }); 74 | window.open(url); 75 | break; 76 | case "id": 77 | url = getLink({ 78 | base: 'http://www.openstreetmap.org/edit?editor=id', 79 | bounds: bounds, 80 | zoom: zoom, 81 | protocol: 'id' 82 | }); 83 | url += "&gpx=" + gpx_url; 84 | if (typeof imagery_url != "undefined") { 85 | // url is supposed to look like tms[22]:http://hiu... 86 | u = imagery_url.substring(imagery_url.indexOf('http')); 87 | u = u.replace('zoom', 'z'); 88 | url += "&background=custom:" + u; 89 | } 90 | window.open(url); 91 | break; 92 | default: 93 | break; 94 | } 95 | }; 96 | $('#export a').live('click', exportOpen); 97 | -------------------------------------------------------------------------------- /OSMTM/static/middlebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/middlebg.png -------------------------------------------------------------------------------- /OSMTM/static/red-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/red-header.png -------------------------------------------------------------------------------- /OSMTM/static/theme/default/style.css: -------------------------------------------------------------------------------- 1 | div.olMap { 2 | z-index: 0; 3 | padding: 0 !important; 4 | margin: 0 !important; 5 | cursor: default; 6 | } 7 | 8 | div.olMapViewport { 9 | text-align: left; 10 | } 11 | 12 | div.olLayerDiv { 13 | -moz-user-select: none; 14 | -khtml-user-select: none; 15 | } 16 | 17 | .olLayerGoogleCopyright { 18 | left: 2px; 19 | bottom: 2px; 20 | } 21 | .olLayerGoogleV3.olLayerGoogleCopyright { 22 | right: auto !important; 23 | } 24 | .olLayerGooglePoweredBy { 25 | left: 2px; 26 | bottom: 15px; 27 | } 28 | .olLayerGoogleV3.olLayerGooglePoweredBy { 29 | bottom: 15px !important; 30 | } 31 | .olControlAttribution { 32 | font-size: smaller; 33 | right: 3px; 34 | bottom: 4.5em; 35 | position: absolute; 36 | display: block; 37 | } 38 | .olControlScale { 39 | right: 3px; 40 | bottom: 3em; 41 | display: block; 42 | position: absolute; 43 | font-size: smaller; 44 | } 45 | .olControlScaleLine { 46 | display: block; 47 | position: absolute; 48 | left: 10px; 49 | bottom: 15px; 50 | font-size: xx-small; 51 | } 52 | .olControlScaleLineBottom { 53 | border: solid 2px black; 54 | border-bottom: none; 55 | margin-top:-2px; 56 | text-align: center; 57 | } 58 | .olControlScaleLineTop { 59 | border: solid 2px black; 60 | border-top: none; 61 | text-align: center; 62 | } 63 | 64 | .olControlPermalink { 65 | right: 3px; 66 | bottom: 1.5em; 67 | display: block; 68 | position: absolute; 69 | font-size: smaller; 70 | } 71 | 72 | div.olControlMousePosition { 73 | bottom: 0em; 74 | right: 3px; 75 | display: block; 76 | position: absolute; 77 | font-family: Arial; 78 | font-size: smaller; 79 | } 80 | 81 | .olControlOverviewMapContainer { 82 | position: absolute; 83 | bottom: 0; 84 | right: 0; 85 | } 86 | 87 | .olControlOverviewMapElement { 88 | padding: 10px 18px 10px 10px; 89 | background-color: #00008B; 90 | -moz-border-radius: 1em 0 0 0; 91 | } 92 | 93 | .olControlOverviewMapMinimizeButton { 94 | right: 0; 95 | bottom: 80px; 96 | cursor: pointer; 97 | } 98 | 99 | .olControlOverviewMapMaximizeButton { 100 | right: 0; 101 | bottom: 80px; 102 | cursor: pointer; 103 | } 104 | 105 | .olControlOverviewMapExtentRectangle { 106 | overflow: hidden; 107 | background-image: url("img/blank.gif"); 108 | cursor: move; 109 | border: 2px dotted red; 110 | } 111 | .olControlOverviewMapRectReplacement { 112 | overflow: hidden; 113 | cursor: move; 114 | background-image: url("img/overview_replacement.gif"); 115 | background-repeat: no-repeat; 116 | background-position: center; 117 | } 118 | 119 | .olLayerGeoRSSDescription { 120 | float:left; 121 | width:100%; 122 | overflow:auto; 123 | font-size:1.0em; 124 | } 125 | .olLayerGeoRSSClose { 126 | float:right; 127 | color:gray; 128 | font-size:1.2em; 129 | margin-right:6px; 130 | font-family:sans-serif; 131 | } 132 | .olLayerGeoRSSTitle { 133 | float:left;font-size:1.2em; 134 | } 135 | 136 | .olPopupContent { 137 | padding:5px; 138 | overflow: auto; 139 | } 140 | 141 | .olControlNavigationHistory { 142 | background-image: url("img/navigation_history.png"); 143 | background-repeat: no-repeat; 144 | width: 24px; 145 | height: 24px; 146 | 147 | } 148 | .olControlNavigationHistoryPreviousItemActive { 149 | background-position: 0 0; 150 | } 151 | .olControlNavigationHistoryPreviousItemInactive { 152 | background-position: 0 -24px; 153 | } 154 | .olControlNavigationHistoryNextItemActive { 155 | background-position: -24px 0; 156 | } 157 | .olControlNavigationHistoryNextItemInactive { 158 | background-position: -24px -24px; 159 | } 160 | 161 | div.olControlSaveFeaturesItemActive { 162 | background-image: url(img/save_features_on.png); 163 | background-repeat: no-repeat; 164 | background-position: 0 1px; 165 | } 166 | div.olControlSaveFeaturesItemInactive { 167 | background-image: url(img/save_features_off.png); 168 | background-repeat: no-repeat; 169 | background-position: 0 1px; 170 | } 171 | 172 | .olHandlerBoxZoomBox { 173 | border: 2px solid red; 174 | position: absolute; 175 | background-color: white; 176 | opacity: 0.50; 177 | font-size: 1px; 178 | filter: alpha(opacity=50); 179 | } 180 | .olHandlerBoxSelectFeature { 181 | border: 2px solid blue; 182 | position: absolute; 183 | background-color: white; 184 | opacity: 0.50; 185 | font-size: 1px; 186 | filter: alpha(opacity=50); 187 | } 188 | 189 | .olControlPanPanel { 190 | top: 10px; 191 | left: 5px; 192 | } 193 | 194 | .olControlPanPanel div { 195 | background-image: url(img/pan-panel.png); 196 | height: 18px; 197 | width: 18px; 198 | cursor: pointer; 199 | position: absolute; 200 | } 201 | 202 | .olControlPanPanel .olControlPanNorthItemInactive { 203 | top: 0; 204 | left: 9px; 205 | background-position: 0 0; 206 | } 207 | .olControlPanPanel .olControlPanSouthItemInactive { 208 | top: 36px; 209 | left: 9px; 210 | background-position: 18px 0; 211 | } 212 | .olControlPanPanel .olControlPanWestItemInactive { 213 | position: absolute; 214 | top: 18px; 215 | left: 0; 216 | background-position: 0 18px; 217 | } 218 | .olControlPanPanel .olControlPanEastItemInactive { 219 | top: 18px; 220 | left: 18px; 221 | background-position: 18px 18px; 222 | } 223 | 224 | .olControlZoomPanel { 225 | top: 71px; 226 | left: 14px; 227 | } 228 | 229 | .olControlZoomPanel div { 230 | background-image: url(img/zoom-panel.png); 231 | position: absolute; 232 | height: 18px; 233 | width: 18px; 234 | cursor: pointer; 235 | } 236 | 237 | .olControlZoomPanel .olControlZoomInItemInactive { 238 | top: 0; 239 | left: 0; 240 | background-position: 0 0; 241 | } 242 | 243 | .olControlZoomPanel .olControlZoomToMaxExtentItemInactive { 244 | top: 18px; 245 | left: 0; 246 | background-position: 0 -18px; 247 | } 248 | 249 | .olControlZoomPanel .olControlZoomOutItemInactive { 250 | top: 36px; 251 | left: 0; 252 | background-position: 0 18px; 253 | } 254 | 255 | /* 256 | * When a potential text is bigger than the image it move the image 257 | * with some headers (closes #3154) 258 | */ 259 | .olControlPanZoomBar div { 260 | font-size: 1px; 261 | } 262 | 263 | .olPopupCloseBox { 264 | background: url("img/close.gif") no-repeat; 265 | cursor: pointer; 266 | } 267 | 268 | .olFramedCloudPopupContent { 269 | padding: 5px; 270 | overflow: auto; 271 | } 272 | 273 | .olControlNoSelect { 274 | -moz-user-select: none; 275 | -khtml-user-select: none; 276 | } 277 | 278 | .olImageLoadError { 279 | background-color: white; 280 | opacity: 0.5; 281 | filter: alpha(opacity=50); /* IE */ 282 | } 283 | 284 | /** 285 | * Cursor styles 286 | */ 287 | 288 | .olCursorWait { 289 | cursor: wait; 290 | } 291 | .olDragDown { 292 | cursor: move; 293 | } 294 | .olDrawBox { 295 | cursor: crosshair; 296 | } 297 | .olControlDragFeatureOver { 298 | cursor: move; 299 | } 300 | .olControlDragFeatureActive.olControlDragFeatureOver.olDragDown { 301 | cursor: -moz-grabbing; 302 | } 303 | 304 | /** 305 | * Layer switcher 306 | */ 307 | .olControlLayerSwitcher { 308 | position: absolute; 309 | top: 25px; 310 | right: 0; 311 | width: 20em; 312 | font-family: sans-serif; 313 | font-weight: bold; 314 | margin-top: 3px; 315 | margin-left: 3px; 316 | margin-bottom: 3px; 317 | font-size: smaller; 318 | color: white; 319 | background-color: transparent; 320 | } 321 | 322 | .olControlLayerSwitcher .layersDiv { 323 | padding-top: 5px; 324 | padding-left: 10px; 325 | padding-bottom: 5px; 326 | padding-right: 75px; 327 | background-color: darkblue; 328 | width: 100%; 329 | height: 100%; 330 | } 331 | 332 | .olControlLayerSwitcher .layersDiv .baseLbl, 333 | .olControlLayerSwitcher .layersDiv .dataLbl { 334 | margin-top: 3px; 335 | margin-left: 3px; 336 | margin-bottom: 3px; 337 | } 338 | 339 | .olControlLayerSwitcher .layersDiv .baseLayersDiv, 340 | .olControlLayerSwitcher .layersDiv .dataLayersDiv { 341 | padding-left: 10px; 342 | } 343 | 344 | .olControlLayerSwitcher .maximizeDiv, 345 | .olControlLayerSwitcher .minimizeDiv { 346 | top: 5px; 347 | right: 0; 348 | cursor: pointer; 349 | } 350 | 351 | .olBingAttribution { 352 | color: #DDD; 353 | } 354 | .olBingAttribution.road { 355 | color: #333; 356 | } 357 | 358 | .olGoogleAttribution.hybrid, .olGoogleAttribution.satellite { 359 | color: #EEE; 360 | } 361 | .olGoogleAttribution { 362 | color: #333; 363 | } 364 | span.olGoogleAttribution a { 365 | color: #77C; 366 | } 367 | span.olGoogleAttribution.hybrid a, span.olGoogleAttribution.satellite a { 368 | color: #EEE; 369 | } 370 | 371 | /** 372 | * Editing and navigation icons. 373 | * (using the editing_tool_bar.png sprint image) 374 | */ 375 | .olControlNavToolbar , 376 | .olControlEditingToolbar { 377 | margin: 5px 5px 0 0; 378 | } 379 | .olControlNavToolbar div, 380 | .olControlEditingToolbar div { 381 | background-image: url("img/editing_tool_bar.png"); 382 | background-repeat: no-repeat; 383 | margin: 0 0 5px 5px; 384 | width: 24px; 385 | height: 22px; 386 | cursor: pointer 387 | } 388 | /* positions */ 389 | .olControlEditingToolbar { 390 | right: 0; 391 | top: 0; 392 | } 393 | .olControlNavToolbar { 394 | top: 295px; 395 | left: 9px; 396 | } 397 | /* layouts */ 398 | .olControlEditingToolbar div { 399 | float: right; 400 | } 401 | /* individual controls */ 402 | .olControlNavToolbar .olControlNavigationItemInactive, 403 | .olControlEditingToolbar .olControlNavigationItemInactive { 404 | background-position: -103px -1px; 405 | } 406 | .olControlNavToolbar .olControlNavigationItemActive , 407 | .olControlEditingToolbar .olControlNavigationItemActive { 408 | background-position: -103px -24px; 409 | } 410 | .olControlNavToolbar .olControlZoomBoxItemInactive { 411 | background-position: -128px -1px; 412 | } 413 | .olControlNavToolbar .olControlZoomBoxItemActive { 414 | background-position: -128px -24px; 415 | } 416 | .olControlEditingToolbar .olControlDrawFeaturePointItemInactive { 417 | background-position: -77px -1px; 418 | } 419 | .olControlEditingToolbar .olControlDrawFeaturePointItemActive { 420 | background-position: -77px -24px; 421 | } 422 | .olControlEditingToolbar .olControlDrawFeaturePathItemInactive { 423 | background-position: -51px -1px; 424 | } 425 | .olControlEditingToolbar .olControlDrawFeaturePathItemActive { 426 | background-position: -51px -24px; 427 | } 428 | .olControlEditingToolbar .olControlDrawFeaturePolygonItemInactive{ 429 | background-position: -26px -1px; 430 | } 431 | .olControlEditingToolbar .olControlDrawFeaturePolygonItemActive { 432 | background-position: -26px -24px; 433 | } 434 | -------------------------------------------------------------------------------- /OSMTM/static/thumb-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/thumb-up.png -------------------------------------------------------------------------------- /OSMTM/static/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/static/thumb.png -------------------------------------------------------------------------------- /OSMTM/templates/about.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">home 3 | <%def name="title()">About 4 | 5 |
6 | 9 |
10 |
11 |

Goal Coordinate Efforts

12 |

OpenStreetMap has been shown to be an effective collection mechanism for infrastructure data. One thing that is lacking is the ability to coordinate workers surveying in the field or working remotely. The goal of the OpenStreetMap Tasking Tool is to make it easy for administrators to define collection areas of interest and collection workflows as well as allowing workers to easily determine what areas they should be working on.

13 |
14 |
15 |

Context

16 |

17 | The current application is the result of an initial work funded by AIFDR. It was imagined by the Humanitarian OpenStreetMap Team which contracted Camptocamp to initiate the work. 18 |

19 |

20 | Then, the project continued to evolve on its own with the help from several developers. 21 |

22 |
23 |
24 | 27 |
28 |
29 | The code is available on Github. 30 |
31 |
32 |
33 |
34 |

Contributors

35 |

36 |

44 |

45 |
46 |
47 |

Softwares This application runs thanks to the following softwares

48 |
49 |
50 |

Client-side

51 |
    52 |
  • OpenLayers
  • 53 |
  • JQuery
  • 54 |
  • Twitter Bootstrap
  • 55 |
  • Less CSS
  • 56 |
57 |

Images

58 | 61 |
62 |
63 |

Server-side

64 |
    65 |
  • Pyramid
  • 66 |
  • SQLite
  • 67 |
  • SQLAlchemy
  • 68 |
  • Shapely
  • 69 |
  • ImpOSM-parser
  • 70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | -------------------------------------------------------------------------------- /OSMTM/templates/admin.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">admin 3 | <%def name="title()">Admin Page 4 |
5 |

Users

6 | Manage users 7 |

Licenses

8 | Manage licenses 9 |
10 | -------------------------------------------------------------------------------- /OSMTM/templates/base.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OSM Tasking Manager - ${self.title()} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 29 | 30 | <% 31 | from pyramid.security import authenticated_userid 32 | from OSMTM.models import DBSession, User 33 | username = authenticated_userid(request) 34 | if username is not None: 35 | user = DBSession().query(User).get(username) 36 | else: 37 | user = None 38 | %> 39 | 40 |
41 | 43 | 62 | 63 |
64 | 65 | feedback 66 | 67 |
68 | 69 |
70 | % if request.session.peek_flash(): 71 |
72 | <% flash = request.session.pop_flash() %> 73 | % for message in flash: 74 | ${message}
75 | % endfor 76 |
77 | % endif 78 | 79 | ${self.body()} 80 | 81 |
82 | 83 |
84 |
85 | 86 | 98 | 99 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /OSMTM/templates/forbidden.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">home 3 | <%def name="title()"> 4 | % if user is not None: 5 | Forbidden 6 | % else: 7 | Login 8 | % endif 9 | 10 | 11 |
12 |
13 |
14 |

About The Tasking Manager

15 |

Coordinate Efforts

16 | OpenStreetMap has been shown to be an effective collection mechanism for infrastructure data. One thing that is lacking is the ability to coordinate workers surveying in the field or working remotely. The goal of the OpenStreetMap Tasking Tool is to make it easy for administrators to define collection areas of interest and collection workflows as well as allowing workers to easily determine what areas they should be working on. 17 |
18 |
19 |

${title()}

20 | % if user is not None: 21 |
22 | Sorry. You're not allowed to view this page! 23 |
24 | % else: 25 | Log in using your OpenStreetMap account » 26 | % endif 27 |
28 |

New to the Tasking Manager?

29 | Take the Tour 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /OSMTM/templates/home.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">home 3 | <%def name="title()">HOT Task Server - Home Page 4 |
5 |
6 |
7 |
8 | 10 | 17 |
18 |
19 | % if admin: 20 |
21 |

22 | Admin page 23 |

24 |

25 | + Create a new job 26 |

27 |
28 | % endif 29 |
30 |
31 |
32 | 33 | No job matches your search criteria 34 | 35 |
36 |
38 | 62 |

63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | private 73 | 74 |

75 |
76 | 77 |

78 | Created by 79 |

80 | 81 |
82 |
83 |
84 |

85 |
86 | % if user.is_admin(): 87 | 109 | % endif 110 |

111 | 112 |   113 | 114 | 115 | Updated ago 116 | 117 |

118 |
119 |
120 |
121 |
122 |
123 | 124 | 125 | 128 | -------------------------------------------------------------------------------- /OSMTM/templates/imagery.mako: -------------------------------------------------------------------------------- 1 | <% 2 | license_accepted = job.license in user.accepted_licenses 3 | %> 4 | % if job.imagery is not None and job.imagery != 'None': 5 |

Imagery

6 | % if license_accepted or not job.license: 7 | <% 8 | type = job.imagery.lower()[:3] 9 | %> 10 |

${job.imagery}

11 | 12 | % if job.imagery_offset_x or job.imagery_offset_y: 13 | Offset: Please beware that the image aligment needs to be modified by the given offset:
${job.imagery_offset_x}:${job.imagery_offset_y}
14 | % endif 15 | % endif 16 | % if job.license: 17 |
18 |

19 | Access to this imagery is limited by the 20 | ${job.license.name} license agreement. 21 |

22 | % if not license_accepted: 23 |

24 | You need to 25 | review and acknowledge 26 | the agreement. 27 |

28 | % endif 29 |
30 | % endif 31 | % endif 32 | -------------------------------------------------------------------------------- /OSMTM/templates/job.edit.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">job_new 3 | <%def name="title()">Edit Job 4 | 5 |
6 |

Edit Job

7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | % for tag in job.tags: 18 | ${tag.tag} 19 | % endfor 20 | Manage tags 21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 |

30 | Note: You can use markdown markup. 31 |

32 |
33 |
34 |
35 |
36 |

Preview

37 | 38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 |

47 | Note: You can use markdown markup. 48 |

49 |
50 |
51 |
52 |
53 |

Preview

54 | 55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 | 63 |

64 | Note: You can use markdown markup. 65 |

66 |
67 |
68 |
69 |
70 |

Preview

71 | 72 |
73 |
74 |
75 | Imagery 76 |
77 | 78 |
79 | 80 |

81 | Note: Follow this format for TMS urls.
tms[22]:http://hiu-maps.net/hot/1.0.0/kathmandu_flipped/{zoom}/{x}/{y}.png 82 |

83 |
84 |
85 |
86 | 87 |
88 | 89 |
90 |
91 |
92 | 93 |
94 | 95 |
96 |
97 |
98 | 99 |
100 | 111 |
112 |
113 |
114 |
115 | Advanced options 116 |
117 | 118 |
119 | 120 |
121 |
122 |
123 | 124 |
125 | 126 |
127 |
128 |
129 |
130 |
131 | 132 |
133 | 134 |
135 |
136 |
137 |
138 |

Put here anything that can be usefull to users while taking a task. 139 |
{x}, {y} and {z} will be replaced by the correponding parameters for each task. 140 |

Ex:

141 |

142 |
143 |
144 |
145 |
146 | 147 |
148 |
149 |
150 | 151 | -------------------------------------------------------------------------------- /OSMTM/templates/job.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | import markdown 3 | %> 4 | <%inherit file="/base.mako"/> 5 | <%def name="id()">job 6 | <%def name="title()">Job - ${job.title} 7 |
8 | 23 | % if job.status == 0: 24 |
25 | Warning! 26 | This job has been archived. You're not supposed to work on it anymore. 27 |
28 | % endif 29 |
30 |
31 | 38 |
39 |
40 |

${markdown.markdown(job.description)|n}

41 |
42 |
43 |

${markdown.markdown(job.workflow)|n}

44 | % if job.imagery: 45 | <%include file="imagery.mako" /> 46 | % endif 47 | % if job.josm_preset: 48 |
49 | Using JOSM? Don't hesitate to use the dedicated preset. 50 |
51 | % endif 52 |
53 |
54 | % if tile is not None: 55 | 60 | % else: 61 | <%include file="/task.empty.mako" /> 62 | % endif 63 |
64 |
65 |
66 |
67 |
    68 | Loading... 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
    82 |
83 |
84 |
85 |
86 |
87 | 88 | 104 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /OSMTM/templates/job.new.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">job_new 3 | <%def name="title()">New Job 4 | 5 |
6 |

New Job

7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | Area of interest 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 | OSM relation ID 24 | 25 | 26 | 27 | 28 | 29 | ex: 1714850 30 | 31 |

32 | Note: You already know an OSM relation which delimits the area. 33 |

34 |
35 |
36 |
37 |
38 | 39 | Draw it yourself 40 |

41 | Note: Draw an area on the map. 42 |

43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 | 61 |

62 | 63 | Up to tiles will be created. 64 | 65 |

66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 |
78 |
79 |
80 | 81 | 84 | 85 | -------------------------------------------------------------------------------- /OSMTM/templates/job.tags.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">job_tags 3 | <%def name="title()">Job tags - ${job.title} 4 | 5 | <% 6 | import json 7 | tags = json.dumps([tag.tag for tag in all_tags]) 8 | %> 9 | 10 |
11 |
12 |

Job tags: ${job.title}

13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 |
    21 | % for tag in job.tags: 22 |
  • ${tag.tag}
  • 23 | % endfor 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /OSMTM/templates/job.task_extra.mako: -------------------------------------------------------------------------------- 1 | <% 2 | content = job.task_extra 3 | content = content.replace('{x}', str(tile.x)) \ 4 | .replace('{y}', str(tile.y)) \ 5 | .replace('{z}', str(tile.zoom)) 6 | %> 7 |
8 |

Extra instructions

9 | ${content|n} 10 | -------------------------------------------------------------------------------- /OSMTM/templates/job.users.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">job_users 3 | <%def name="title()">Job whitelist - ${job.title} 4 | 5 |
6 |
7 |

Job whitelist: ${job.title}

8 |

This job is 9 | % if job.is_private: 10 | private. 11 | % else: 12 | public. 13 | % endif 14 |

15 |
16 |
17 | 18 | 23 | 24 |
25 |
26 |
    27 | % for user in job.users: 28 |
  • ${user.username}
  • 29 | % endfor 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /OSMTM/templates/license.edit.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">license_new 3 | <%def name="title()">Edit Imagery License 4 |
5 |

Edit License

6 |
7 |
8 | 9 |
10 | 12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | Delete 40 |
41 |
42 |
43 | 51 | -------------------------------------------------------------------------------- /OSMTM/templates/license.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">license 3 | <%def name="title()">${license.name} License Acknowledgement 4 |
5 |
6 |

${license.name} License Acknowledgement

7 |

Access via this site to imagery identified as "${license.name}" is subject to the following usage terms:

8 |
9 |

“${license.description}”

10 | % if license.plain_text != None and license.plain_text != '': 11 | ${license.plain_text} 12 | % endif 13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /OSMTM/templates/licenses.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">licenses 3 | <%def name="title()">Admin - Licenses 4 |
5 |

Licenses

6 |
7 |
8 |
    9 | % for license in licenses: 10 |
  • ${license.name} 11 | edit 12 |
  • 13 | % endfor 14 |
15 | 16 |
17 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /OSMTM/templates/task.comments.mako: -------------------------------------------------------------------------------- 1 | <% 2 | steps = [] 3 | prev_change = False 4 | for i, step in enumerate(history): 5 | # exclude last one 6 | if i == 0: 7 | continue 8 | # don't show unlocks which follow a change 9 | if prev_change: 10 | prev_change = False 11 | continue 12 | steps.append(step) 13 | prev_change = True if step.change else False 14 | if tile.version != 1 and not prev_change: 15 | steps.append(tile) 16 | steps.reverse() 17 | %> 18 |
19 |

History

20 | % if len(steps) > 0: 21 | <% 22 | from OSMTM.utils import timesince 23 | %> 24 | % for i, step in enumerate(steps): 25 | <% 26 | first = "first" if i == 0 else "" 27 | last = "last" if i == len(steps) - 1 else "" 28 | %> 29 |
30 |

31 | 32 | % if step.checkout: 33 | Locked 34 | % elif step.change: 35 | % if step.checkin == 0: 36 | Invalidated 37 | % elif step.checkin == 2: 38 | Validated 39 | % elif step.checkin == 1: 40 | Marked as done 41 | % endif 42 | % else: 43 | Unlocked 44 | % endif 45 | 46 | % if step.change or step.checkout: 47 | by 48 | ${step.username} 49 |

50 | % endif 51 | % if step.comment is not None: 52 |
53 | ${step.comment} 54 |
55 | % endif 56 |

57 | <% 58 | time_ago = timesince(step.update) 59 | %> 60 | ${time_ago} ago 61 |

62 |
63 | % endfor 64 | <% 65 | minx, miny, maxx, maxy = tile.to_polygon(4326).bounds 66 | %> 67 |

OSM changesets

68 | % else: 69 | Nothing has happen yet. 70 | % endif 71 | -------------------------------------------------------------------------------- /OSMTM/templates/task.empty.mako: -------------------------------------------------------------------------------- 1 | % if error_msg is not UNDEFINED and error_msg is not None: 2 |
3 | ${error_msg} 4 |
5 | % endif 6 |

7 | Pick a task randomly 9 |

10 |

11 | Or choose one by clicking on the map. 12 |

13 | 14 |
15 |

If you're an experienced mapper, you can also be given a task to validate.

16 | 20 | % if split_id is not UNDEFINED: 21 | <% 22 | from geojson import dumps 23 | tiles = dumps(new_tiles) 24 | %> 25 | 28 | % endif 29 | -------------------------------------------------------------------------------- /OSMTM/templates/task.gpx.mako: -------------------------------------------------------------------------------- 1 | 2 | <%! 3 | import datetime 4 | %><% 5 | timestamp = datetime.datetime.now() 6 | timestamp = timestamp.isoformat() 7 | %> 8 | 9 | 10 | 11 | HOT Tasking Manager 12 | 13 | 14 | 15 | 16 | Task tile for job ${job_id} 17 | 18 | % for point in polygon.exterior.coords: 19 | 20 | % endfor 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /OSMTM/templates/task.mako: -------------------------------------------------------------------------------- 1 |
2 | Loading 3 |
4 | <%! 5 | import markdown 6 | %> 7 | <% 8 | # allow editing only if a task is taken by a user or wating for validation 9 | if (tile.checkout and tile.user == user) or tile.checkin == 1: 10 | disabled = '' 11 | else: 12 | disabled = 'disabled' 13 | %> 14 | % if not tile: 15 | <%include file="/task.empty.mako" /> 16 | % else: 17 |
18 |

19 | % if (tile.checkout and tile.user == user) or tile.checkin == 1: 20 |

21 | % else: 22 |

23 | % endif 24 | 25 | JOSM 26 | iD Editor 27 | Potlatch 2 28 | Walking Papers 29 | .osm 30 | .gpx 31 |

32 |
33 |
34 | % if tile.username == user.username or tile.checkin >= 1: 35 | 36 | <% 37 | if tile.username == user.username: 38 | comment_label = 'Please add a comment' 39 | else: 40 | comment_label = 'Please write why you marked this tile as invalid so that the user may eventually correct his mistakes if any.' 41 | %> 42 |
43 | % if tile.username == user.username: 44 |

45 | min. left 48 | You locked this task. 49 | 50 |

51 |

52 |
53 |

54 | % elif tile.checkin >= 1: 55 | 59 | % if tile.checkin == 1: 60 | 64 | % endif 65 | Clear selection 66 | % endif 67 | 81 | 82 | % else: 83 | % if current_task is not None: 84 |

85 | 86 | You already have a task locked. 87 |

88 | % endif 89 | % if tile.checkout != True and tile.checkin != 2 and current_task is None: 90 | <% 91 | disabled = "" 92 | tooltip = "" 93 | if current_task is not None: 94 | disabled = "disabled" 95 | tooltip = "You cannot lock more than one task" 96 | else: 97 | tooltip = "Lock this task to notify others that you are currently working on it." 98 | %> 99 |

100 | Yes, I want to work on this task 103 |

104 | % elif tile.username is not None: 105 |

106 | Already locked by ${tile.username}. 107 |

108 | % endif 109 | % if tile.checkin == 0 and (tile.zoom - job.zoom) < 1 and tile.username is None: 110 |

111 | Split! 112 | % endif 113 | Clear selection 114 |

115 | % endif 116 | % if job.task_extra is not None: 117 | <%include file="job.task_extra.mako" /> 118 | % endif 119 | <%include file="task.comments.mako" /> 120 |
121 | 139 | % endif 140 | 143 | -------------------------------------------------------------------------------- /OSMTM/templates/task.osm.mako: -------------------------------------------------------------------------------- 1 | 2 | <% 3 | id = -2 4 | %> 5 | % for point in polygon.exterior.coords: 6 | 7 | <% id = id -1 %> 8 | % endfor 9 | 10 | % for i in [-2, -3, -4, -5, -6]: 11 | 12 | % endfor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /OSMTM/templates/tour.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">tour 3 | <%def name="title()">Tour 4 | 6 | 8 | 10 |
11 |
12 |
13 |
14 | 76 |
77 |
78 |
79 | 85 | -------------------------------------------------------------------------------- /OSMTM/templates/user.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">user 3 | <%def name="title()">User Profile 4 |
5 |

Profile for ${user.username} 6 |

7 |
8 |

9 | % if user.is_admin(): 10 | This user is an administrator. 11 | % else: 12 | This user has no admin privileges. 13 | % endif 14 | 15 | % if admin: 16 | Edit privileges 17 | % endif 18 |

19 |
20 |
21 |

22 | 23 | [OSM] OSM Profile 24 |

25 |
26 |
27 |

Jobs

28 | % if jobs: 29 |
    30 | % for job_info in jobs: 31 |
  • ${job_info["job"].title} (${job_info["count"]} tiles) 32 |
  • 33 | % endfor 34 |
35 | % else: 36 | User hasn't contribute yet. 37 | % endif 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /OSMTM/templates/user_edit.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">user 3 | <%def name="title()">User Profile 4 |
5 | % if admin: 6 |

Profile for ${user.username}

7 |
8 | % else: 9 |

Profile

10 | 11 | % endif 12 |
13 | 14 |
15 | % if admin or user.is_admin(): 16 | 24 | % endif 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /OSMTM/templates/users.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.mako"/> 2 | <%def name="id()">users 3 | <%def name="title()">Registered Users 4 |
5 |

Registered Users

6 |
7 |
8 |
    9 | % for user in users: 10 |
  • ${user.username} 11 | % if admin: 12 | edit 13 | % endif 14 |
  • 15 | % endfor 16 |
17 |
18 | % if admin: 19 |
20 |
21 | 22 | 23 |

24 | Note: the user will be created if it doesn't already exist. 25 |

26 |
27 |
28 | % endif 29 |
30 |
31 | -------------------------------------------------------------------------------- /OSMTM/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyramid.config import Configurator 3 | from pyramid import testing 4 | 5 | def _initTestingDB(): 6 | from sqlalchemy import create_engine 7 | from OSMTM.models import initialize_sql, populate 8 | session = initialize_sql(create_engine('sqlite:///:memory:')) 9 | populate() 10 | return session 11 | 12 | def _registerRoutes(config): 13 | config.add_route('job', 'job/{job}') 14 | 15 | class TileModelTests(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.config = testing.setUp() 19 | _initTestingDB() 20 | 21 | def tearDown(self): 22 | testing.tearDown() 23 | 24 | def _getTargetClass(self): 25 | from OSMTM.models import Tile 26 | return Tile 27 | 28 | def _makeOne(self, x=1, y=2): 29 | return self._getTargetClass()(x, y) 30 | 31 | def test_constructor(self): 32 | instance = self._makeOne() 33 | self.assertEqual(instance.x, 1) 34 | self.assertEqual(instance.y, 2) 35 | self.assertEqual(instance.checkin, 0) 36 | 37 | class JobModelTests(unittest.TestCase): 38 | 39 | def setUp(self): 40 | self.config = testing.setUp() 41 | _initTestingDB() 42 | 43 | def tearDown(self): 44 | testing.tearDown() 45 | 46 | def _getTargetClass(self): 47 | from OSMTM.models import Job 48 | return Job 49 | 50 | def _makeOne(self, title='SomeTitle', 51 | short_description='a short description', description='some description', 52 | workflow='some workflow', geometry='POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))', zoom=1): 53 | return self._getTargetClass()(title, short_description, description, workflow, geometry, zoom) 54 | 55 | def test_constructor(self): 56 | instance = self._makeOne() 57 | self.assertEqual(instance.title, 'SomeTitle') 58 | self.assertEqual(instance.status, 1) 59 | self.assertEqual(instance.description, 'some description') 60 | self.assertEqual(instance.geometry, 'POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))') 61 | self.assertEqual(instance.workflow, 'some workflow') 62 | self.assertEqual(instance.zoom, 1) 63 | 64 | class UserModelTests(unittest.TestCase): 65 | 66 | def setUp(self): 67 | self.config = testing.setUp() 68 | _initTestingDB() 69 | 70 | def tearDown(self): 71 | testing.tearDown() 72 | 73 | def _getTargetClass(self): 74 | from OSMTM.models import User 75 | return User 76 | 77 | def _makeOne(self, username=u'bar'): 78 | user = self._getTargetClass()(username) 79 | return user 80 | 81 | def test_constructor(self): 82 | instance = self._makeOne() 83 | self.assertEqual(instance.username, u'bar') 84 | 85 | class TestHome(unittest.TestCase): 86 | def setUp(self): 87 | self.config = testing.setUp() 88 | _initTestingDB() 89 | 90 | def tearDown(self): 91 | testing.tearDown() 92 | 93 | def test_it(self): 94 | from OSMTM.views.views import home 95 | request = testing.DummyRequest() 96 | self.config.testing_securitypolicy(userid=u'foo') 97 | info = home(request) 98 | self.assertEqual(len(info['jobs']), 1) 99 | self.assertEqual(info['admin'], False) 100 | 101 | class TestJobNew(unittest.TestCase): 102 | 103 | def setUp(self): 104 | self.config = testing.setUp() 105 | self.session = _initTestingDB() 106 | 107 | def tearDown(self): 108 | testing.tearDown() 109 | 110 | def test_it(self): 111 | _registerRoutes(self.config) 112 | from OSMTM.views.jobs import job_new 113 | request = testing.DummyRequest() 114 | request.params = { 115 | 'form.submitted': True, 116 | 'title':u'NewJob', 117 | 'short_description':u'SomeShortDescription', 118 | 'description':u'SomeDescription', 119 | 'geometry':u'POLYGON((0 0, 100 0, 100 100, 0 100, 0 0))', 120 | 'workflow':u'SomeWorflow', 121 | 'imagery':u'', 122 | 'zoom':20 123 | } 124 | response = job_new(request) 125 | self.assertEqual(response.location, 'http://example.com/job/2') 126 | from OSMTM.models import Job 127 | self.assertEqual(len(self.session.query(Job).get(2).tiles), 128 | 9) 129 | 130 | class TestJob(unittest.TestCase): 131 | 132 | def setUp(self): 133 | self.config = testing.setUp() 134 | self.session = _initTestingDB() 135 | 136 | def tearDown(self): 137 | testing.tearDown() 138 | 139 | def test_it(self): 140 | _registerRoutes(self.config) 141 | from OSMTM.views.jobs import job 142 | request = testing.DummyRequest() 143 | self.config.testing_securitypolicy(userid=u'foo') 144 | request.matchdict = {'job': 1} 145 | info = job(request) 146 | from OSMTM.models import Job 147 | self.assertEqual(info['job'], self.session.query(Job).get(1)) 148 | 149 | class FunctionalTests(unittest.TestCase): 150 | 151 | def setUp(self): 152 | from OSMTM import main 153 | settings = { 154 | 'sqlalchemy.url': 'sqlite:///:memory:' 155 | } 156 | self.app = main({}, **settings) 157 | from webtest import TestApp 158 | self.testapp = TestApp(self.app) 159 | 160 | from OSMTM.models import populate 161 | populate() 162 | 163 | def __remember(self, username): 164 | from pyramid.security import remember 165 | request = testing.DummyRequest(environ={'SERVER_NAME': 'servername'}) 166 | request.registry = self.app.registry 167 | headers = remember(request, username, max_age=2*7*24*60*60) 168 | return {'Cookie': headers[0][1].split(';')[0]} 169 | 170 | def __forget(self): 171 | from pyramid.security import forget 172 | request = testing.DummyRequest(environ={'SERVER_NAME': 'servername'}) 173 | request.registry = self.app.registry 174 | forget(request) 175 | 176 | def test_root(self): 177 | res = self.testapp.get('/', status=200) 178 | self.failUnless('About Task Server' in res.body) 179 | self.failUnless('Login' in res.body) 180 | 181 | def test_authenticated(self): 182 | headers = self.__remember('foo') 183 | try: 184 | res = self.testapp.get('/', headers=headers, status=200) 185 | finally: 186 | self.__forget() 187 | self.failUnless('You are foo' in res.body) 188 | 189 | def test_user_authenticated(self): 190 | headers = self.__remember('foo') 191 | try: 192 | res = self.testapp.get('/', headers=headers, status=200) 193 | finally: 194 | self.__forget() 195 | self.assertFalse('Users' in res.body) 196 | 197 | def test_user_users(self): 198 | headers = self.__remember('foo') 199 | try: 200 | res = self.testapp.get('/users', headers=headers, status=200) 201 | finally: 202 | self.__forget() 203 | 204 | def test_user_profile(self): 205 | headers = self.__remember('foo') 206 | try: 207 | res = self.testapp.get('/user/foo', headers=headers, status=200) 208 | finally: 209 | self.__forget() 210 | self.assertTrue('Forbidden' in res.body) 211 | 212 | def test_admin_authenticated(self): 213 | headers = self.__remember('admin_user') 214 | try: 215 | res = self.testapp.get('/', headers=headers, status=200) 216 | finally: 217 | self.__forget() 218 | self.assertTrue('Users' in res.body) 219 | 220 | def test_about(self): 221 | headers = self.__remember('foo') 222 | try: 223 | res = self.testapp.get('/about', headers=headers, status=200) 224 | finally: 225 | self.__forget() 226 | self.assertEquals(res.html.head.title.string, 'OSM Tasking Manager - About') 227 | 228 | def test_admin_user(self): 229 | headers = self.__remember('admin_user') 230 | try: 231 | res = self.testapp.get('/user/foo', headers=headers, status=200) 232 | self.assertTrue('Profile for foo' in res.body) 233 | self.assertFalse(res.html.find(id='admin').checked) 234 | finally: 235 | self.__forget() 236 | 237 | try: 238 | res = self.testapp.get('/user/admin_user', headers=headers, status=200) 239 | self.assertTrue('Profile for admin_user' in res.body) 240 | self.assertTrue(res.html.find(id='admin')['checked'] == 'checked') 241 | finally: 242 | self.__forget() 243 | 244 | def test_admin_user_update(self): 245 | headers = self.__remember('admin_user') 246 | try: 247 | res = self.testapp.get('/user/foo', 248 | headers=headers, status=200) 249 | res.form['admin'].checked = True 250 | res2 = res.form.submit('form.submitted', headers=headers, 251 | status=302) 252 | res3 = res2.follow(headers=headers, status=200) 253 | self.assertTrue('Profile for foo' in res3.body) 254 | self.assertTrue(res3.form['admin'].checked) 255 | finally: 256 | self.__forget() 257 | 258 | ######### 259 | # tasks # 260 | ######### 261 | 262 | def test_task_not_found(self): 263 | headers = self.__remember('foo') 264 | try: 265 | res = self.testapp.get('/job/1/task/1/1', headers=headers, 266 | status=404) 267 | finally: 268 | self.__forget() 269 | 270 | def test_task(self): 271 | headers = self.__remember('foo') 272 | try: 273 | res = self.testapp.get('/job/1/task/32774/42026', headers=headers, 274 | status=302) 275 | self.assertEquals(res.location, 'http://localhost/job/1') 276 | finally: 277 | self.__forget() 278 | 279 | def test_task_take(self): 280 | headers = self.__remember('foo') 281 | try: 282 | res = self.testapp.get('/job/1/task/32774/42026/take', headers=headers, 283 | status=302) 284 | self.assertEquals(res.location, 285 | 'http://localhost/job/1/task/32774/42026') 286 | res2 = self.testapp.get(res.location, headers=headers, 287 | status=200) 288 | 289 | form = res2.form 290 | res3 = form.submit(headers=headers, status=302) 291 | res4 = res3.follow(headers=headers, status=200) 292 | from OSMTM.models import DBSession, Tile 293 | session = DBSession() 294 | tile = session.query(Tile).get((32774, 42026, 1)) 295 | self.assertEquals(tile.checkin, 1) 296 | 297 | res5 = form.submit(headers=headers) 298 | tile = session.query(Tile).get((32774, 42026, 1)) 299 | self.assertEquals(tile.checkin, 1) 300 | finally: 301 | self.__forget() 302 | -------------------------------------------------------------------------------- /OSMTM/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from math import floor, ceil, pi, atan, exp 3 | from shapely.geometry import Polygon 4 | 5 | import logging 6 | log = logging.getLogger(__name__) 7 | 8 | # Maximum resolution 9 | MAXRESOLUTION = 156543.0339 10 | 11 | # X/Y axis limit 12 | max_limit = MAXRESOLUTION * 256 / 2 13 | 14 | 15 | class TileBuilder(object): 16 | def __init__(self, parameter): 17 | self.a = parameter 18 | 19 | def create_square(self, i, j, srs=900913): 20 | """ 21 | creates a Shapely Polygon geometry representing tile indexed by (i,j) 22 | in OSMQA v2 with dimension a 23 | """ 24 | xmin = i * self.a - max_limit 25 | ymin = j * self.a - max_limit 26 | xmax = (i + 1) * self.a - max_limit 27 | ymax = (j + 1) * self.a - max_limit 28 | if srs == 4326: 29 | xmin, ymin = transform_900913_to_4326(xmin, ymin) 30 | xmax, ymax = transform_900913_to_4326(xmax, ymax) 31 | return Polygon( 32 | [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)] 33 | ) 34 | 35 | 36 | def get_tiles_in_geom(geom, z): 37 | """ 38 | This method finds the tiles that intersect the given geometry for the 39 | given zoom 40 | """ 41 | xmin = geom.bounds[0] 42 | ymin = geom.bounds[1] 43 | xmax = geom.bounds[2] 44 | ymax = geom.bounds[3] 45 | 46 | # tile size (in meters) at the required zoom level 47 | step = max_limit / (2 ** (z - 1)) 48 | 49 | xminstep = int(floor((xmin + max_limit) / step)) 50 | xmaxstep = int(ceil((xmax + max_limit) / step)) 51 | yminstep = int(floor((ymin + max_limit) / step)) 52 | ymaxstep = int(ceil((ymax + max_limit) / step)) 53 | 54 | tb = TileBuilder(step) 55 | polygons = [] 56 | tiles = [] 57 | for i in range(xminstep, xmaxstep + 1): 58 | for j in range(yminstep, ymaxstep + 1): 59 | polygon = tb.create_square(i, j) 60 | if geom.intersects(polygon): 61 | polygons.append(polygon) 62 | tiles.append((i, j)) 63 | return tiles 64 | 65 | 66 | def transform_900913_to_4326(x, y): 67 | """ 68 | Transforms pair of mercator coordinates to lonlat 69 | """ 70 | lon = (x / 20037508.34) * 180 71 | lat = (y / 20037508.34) * 180 72 | 73 | lat = 180 / pi * (2 * atan(exp(lat * pi / 180)) - pi / 2) 74 | 75 | return lon, lat 76 | 77 | 78 | def ungettext(a, b, count): 79 | if count: 80 | return b 81 | return a 82 | 83 | 84 | def ugettext(a): 85 | return a 86 | 87 | 88 | def timesince(d, now=None): 89 | if d is None: 90 | return 91 | """ 92 | Takes two datetime objects and returns the time between d and now 93 | as a nicely formatted string, e.g. "10 minutes". If d occurs after now, 94 | then "0 minutes" is returned. 95 | 96 | Units used are years, months, weeks, days, hours, and minutes. 97 | Seconds and microseconds are ignored. Up to two adjacent units will be 98 | displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are 99 | possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. 100 | 101 | Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since 102 | """ 103 | chunks = ( 104 | (60 * 60 * 24 * 365, lambda n: ungettext('year', 'years', n)), 105 | (60 * 60 * 24 * 30, lambda n: ungettext('month', 'months', n)), 106 | (60 * 60 * 24 * 7, lambda n: ungettext('week', 'weeks', n)), 107 | (60 * 60 * 24, lambda n: ungettext('day', 'days', n)), 108 | (60 * 60, lambda n: ungettext('hour', 'hours', n)), 109 | (60, lambda n: ungettext('minute', 'minutes', n)) 110 | ) 111 | # Convert datetime.date to datetime.datetime for comparison. 112 | if not isinstance(d, datetime.datetime): 113 | d = datetime.datetime(d.year, d.month, d.day) 114 | if now and not isinstance(now, datetime.datetime): 115 | now = datetime.datetime(now.year, now.month, now.day) 116 | 117 | if not now: 118 | if d.tzinfo: 119 | now = datetime.datetime.now(LocalTimezone(d)) 120 | else: 121 | now = datetime.datetime.now() 122 | 123 | # ignore microsecond part of 'd' since we removed it from 'now' 124 | delta = now - (d - datetime.timedelta(0, 0, d.microsecond)) 125 | since = delta.days * 24 * 60 * 60 + delta.seconds 126 | if since <= 0: 127 | # d is in the future compared to now, stop processing. 128 | return u'0 ' + ugettext('minutes') 129 | for i, (seconds, name) in enumerate(chunks): 130 | count = since // seconds 131 | if count != 0: 132 | break 133 | s = ugettext( 134 | '%(number)d %(type)s') % {'number': count, 'type': name(count)} 135 | 136 | if i + 1 < len(chunks): 137 | # Now get the second item 138 | seconds2, name2 = chunks[i + 1] 139 | count2 = (since - (seconds * count)) // seconds2 140 | if count2 != 0: 141 | s += ugettext(', %(number)d %(type)s') % { 142 | 'number': count2, 'type': name2(count2) 143 | } 144 | return s 145 | 146 | 147 | def parse_float(input_value, default_output=0): 148 | """ 149 | Float parsing utility function 150 | 151 | :arg input_value: the value to parse 152 | :type input_value: string 153 | 154 | :arg default_output: returned value if input is not parsable 155 | :type default_output: float 156 | 157 | :returns: a float number 158 | :rtype: float 159 | """ 160 | try: 161 | output = float(input_value) 162 | except Exception, e: 163 | # catch all exceptions 164 | log.debug('Could not parse float value, argument %s', input_value) 165 | log.debug('Exception: %s', e.message) 166 | 167 | output = default_output 168 | 169 | return output 170 | -------------------------------------------------------------------------------- /OSMTM/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/OSMTM/views/__init__.py -------------------------------------------------------------------------------- /OSMTM/views/admin.py: -------------------------------------------------------------------------------- 1 | from pyramid.view import view_config 2 | from pyramid.url import route_url 3 | 4 | from pyramid.security import remember, forget, authenticated_userid 5 | 6 | from OSMTM.models import License 7 | 8 | @view_config(route_name='admin', renderer='admin.mako', permission='edit') 9 | def admin(request): 10 | return dict() 11 | -------------------------------------------------------------------------------- /OSMTM/views/crossdomain.py: -------------------------------------------------------------------------------- 1 | import os 2 | from webob import Response 3 | 4 | def crossdomain_view(request): 5 | here = os.path.dirname(__file__) 6 | file = open(os.path.join(here, '../static', 'crossdomain.xml')) 7 | return Response(content_type='text/xml', app_iter=file) 8 | -------------------------------------------------------------------------------- /OSMTM/views/license.py: -------------------------------------------------------------------------------- 1 | from pyramid.httpexceptions import HTTPFound 2 | from pyramid.view import view_config 3 | from pyramid.url import route_url 4 | 5 | from pyramid.security import remember, forget, authenticated_userid 6 | 7 | from OSMTM.models import DBSession 8 | from OSMTM.models import User 9 | from OSMTM.models import License 10 | 11 | @view_config(route_name='licenses', renderer='licenses.mako', permission='edit') 12 | def licenses(request): 13 | session = DBSession() 14 | licenses = session.query(License).all() 15 | return dict(licenses=licenses) 16 | 17 | @view_config(route_name='license', renderer='license.mako', permission='edit') 18 | def license(request): 19 | session = DBSession() 20 | id = request.matchdict['license'] 21 | license = session.query(License).get(id) 22 | 23 | username = authenticated_userid(request) 24 | user = session.query(User).get(username) 25 | redirect = request.params.get("redirect", request.route_url("home")) 26 | if "accepted_terms" in request.params: 27 | if request.params["accepted_terms"] == "I AGREE": 28 | user.accepted_licenses.append(license) 29 | elif license in user.accepted_licenses: 30 | user.accepted_licenses.remove(license) 31 | return HTTPFound(location=redirect) 32 | else: 33 | return dict(user=user, license=license, redirect=redirect) 34 | 35 | @view_config(route_name='license_new', permission='admin') 36 | def license_new(request): 37 | session = DBSession() 38 | license = License() 39 | license.name = '' 40 | license.description = '' 41 | license.plain_text = '' 42 | 43 | session.add(license) 44 | session.flush() 45 | return HTTPFound(location = route_url('license_edit', request, license=license.id)) 46 | 47 | @view_config(route_name='license_delete', permission='admin') 48 | def license_delete(request): 49 | session = DBSession() 50 | id = request.matchdict['license'] 51 | license = session.query(License).get(id) 52 | 53 | session.delete(license) 54 | session.flush() 55 | request.session.flash('License removed!') 56 | return HTTPFound(location = route_url('licenses', request)) 57 | 58 | @view_config(route_name='license_edit', renderer='license.edit.mako', 59 | permission='admin') 60 | def license_edit(request): 61 | id = request.matchdict['license'] 62 | session = DBSession() 63 | license = session.query(License).get(id) 64 | 65 | if 'form.submitted' in request.params: 66 | license.name = request.params['name'] 67 | license.description = request.params['description'] 68 | license.plain_text = request.params['plain_text'] 69 | 70 | session.add(license) 71 | request.session.flash('License updated!') 72 | return dict(license=license) 73 | -------------------------------------------------------------------------------- /OSMTM/views/osmproxy.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import urllib 3 | 4 | from pyramid.view import view_config 5 | from pyramid.httpexceptions import (HTTPBadRequest) 6 | from pyramid.response import Response 7 | 8 | from imposm.parser import OSMParser 9 | from shapely.geometry import Polygon 10 | from shapely.geometry import MultiPolygon 11 | 12 | from geojson import Feature, FeatureCollection 13 | 14 | import logging 15 | log = logging.getLogger(__file__) 16 | 17 | @view_config(route_name='osmproxy', renderer='geojson') 18 | def osmproxy(request): 19 | url = request.params.get("url") 20 | if url is None: 21 | return HTTPBadRequest() 22 | 23 | # instantiate parser and parser and start parsing 24 | parser = RelationParser() 25 | p = OSMParser(concurrency=1, 26 | coords_callback=parser.get_coords, 27 | relations_callback=parser.get_relations, 28 | ways_callback=parser.get_ways) 29 | 30 | temp = tempfile.NamedTemporaryFile(suffix='.osm') 31 | urllib.urlretrieve(url, temp.name) 32 | p.parse(temp.name) 33 | temp.close() 34 | 35 | polygons = [] 36 | r = parser.relation 37 | 38 | # first check for self closing ways 39 | for i in range(len(r) - 1, 0, -1): 40 | w = parser.ways[r[i]] 41 | if w[len(w) - 1] == w[0]: 42 | r.pop(i) 43 | nodes = [] 44 | polygon = Polygon([parser.nodes[node] for node in w]) 45 | polygons.append(polygon) 46 | 47 | if len(r) > 0: 48 | prev = parser.ways[r[0]] 49 | ordered_ways = [] 50 | ordered_ways.append(prev) 51 | r.pop(0) 52 | while len(r): 53 | match = False 54 | for i in range(0, len(r)): 55 | w = parser.ways[r[i]] 56 | # first node of the next way matches the last of the previous one 57 | if w[0] == prev[len(prev) - 1]: 58 | match = w 59 | # or maybe the way has to be reversed 60 | elif w[len(w) - 1] == prev[len(prev) - 1]: 61 | match = w[::-1] 62 | if match: 63 | prev = match 64 | ordered_ways.append(match) 65 | r.pop(i) 66 | break 67 | 68 | if len(ordered_ways) > 0: 69 | # now that ways are correctly ordered, we can create a unique geometry 70 | nodes = [] 71 | for way in ordered_ways: 72 | for node in way: 73 | nodes.append(parser.nodes[node]) 74 | # make sure that first and last node are similar 75 | if nodes[0] != nodes[len(nodes) - 1]: 76 | raise 77 | # create a shapely polygon with the nodes 78 | polygons.append(Polygon(nodes)) 79 | 80 | multipolygon = MultiPolygon(polygons) 81 | return FeatureCollection([Feature(geometry=multipolygon)]) 82 | 83 | # simple class that handles the parsed OSM data. 84 | class RelationParser(object): 85 | def __init__(self): 86 | self.nodes = {} 87 | self.ways = {} 88 | self.relation = [] 89 | 90 | def get_coords(self, coords): 91 | # callback method for nodes 92 | for osm_id, lon, lat in coords: 93 | self.nodes[osm_id] = (lon, lat) 94 | 95 | def get_ways(self, ways): 96 | # callback method for ways 97 | for way in ways: 98 | self.ways[way[0]] = way[2] 99 | 100 | def get_relations(self, relations): 101 | # callback method for relations 102 | # there should be only one in our case 103 | if len(relations) == 0: 104 | return 105 | for member in relations[0][2]: 106 | if member[1] == 'way': 107 | self.relation.append(member[0]) 108 | -------------------------------------------------------------------------------- /OSMTM/views/security.py: -------------------------------------------------------------------------------- 1 | from pyramid.security import authenticated_userid 2 | 3 | def login(request): 4 | return dict(user=authenticated_userid(request)) 5 | -------------------------------------------------------------------------------- /OSMTM/views/tasks.py: -------------------------------------------------------------------------------- 1 | from pyramid.httpexceptions import HTTPFound 2 | from pyramid.httpexceptions import HTTPNotFound 3 | from pyramid.view import view_config 4 | from pyramid.url import route_url 5 | 6 | from OSMTM.models import DBSession 7 | from OSMTM.models import Tile 8 | from OSMTM.models import TileHistory 9 | from OSMTM.models import User 10 | from OSMTM.models import Job 11 | 12 | from OSMTM.views.views import EXPIRATION_DURATION, checkTask 13 | 14 | from geojson import Feature, FeatureCollection 15 | from sqlalchemy.sql.expression import and_ 16 | from sqlalchemy.orm.exc import NoResultFound 17 | 18 | from datetime import datetime 19 | import random 20 | 21 | from pyramid.security import authenticated_userid 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | @view_config(route_name="task_empty", renderer="task.empty.mako", permission="job") 27 | def task_empty(request): 28 | id = request.matchdict['job'] 29 | session = DBSession() 30 | job = session.query(Job).get(id) 31 | return dict(job=job) 32 | 33 | @view_config(route_name='task_xhr', renderer='task.mako', permission='job', 34 | http_cache=0) 35 | def task_xhr(request): 36 | job_id = request.matchdict['job'] 37 | x = request.matchdict['x'] 38 | y = request.matchdict['y'] 39 | zoom = request.matchdict['zoom'] 40 | session = DBSession() 41 | tile = session.query(Tile).get((x, y, zoom, job_id)) 42 | if tile is None: 43 | return HTTPNotFound() 44 | username = authenticated_userid(request) 45 | user = session.query(User).get(username) 46 | time_left = 'null' 47 | #if tile.user != user: 48 | #request.session.flash('You cannot see this task.') 49 | #return HTTPFound(location=request.route_url('job', job=job_id)) 50 | if tile.update: 51 | time_left = (tile.update - (datetime.now() - EXPIRATION_DURATION)) \ 52 | .seconds 53 | filter = and_(TileHistory.x==x, TileHistory.y==y, TileHistory.job_id==job_id) 54 | history = session.query(TileHistory).filter(filter)\ 55 | .order_by(TileHistory.update)\ 56 | .all() 57 | 58 | current_task = get_locked_task(job_id, username) 59 | log.debug( 60 | 'Tile username: %s, checkout: %s, checkin: %s', 61 | tile.username, tile.checkout, tile.checkin 62 | ) 63 | return dict(tile=tile, 64 | current_task=current_task, 65 | history=history, 66 | time_left=time_left, 67 | user=user, 68 | job=tile.job) 69 | 70 | @view_config(route_name='task_done', permission='job', renderer='json') 71 | def done(request): 72 | job_id = request.matchdict['job'] 73 | x = request.matchdict['x'] 74 | y = request.matchdict['y'] 75 | zoom = request.matchdict['zoom'] 76 | session = DBSession() 77 | username = authenticated_userid(request) 78 | tile = session.query(Tile).get((x, y, zoom, job_id)) 79 | tile.comment = request.params['comment'] 80 | if 'invalidate' in request.params: 81 | # task goes back to the queue 82 | tile.checkin = 0 83 | tile.username = username 84 | elif 'validate' in request.params: 85 | # task goes back to the queue 86 | tile.checkin = 2 87 | tile.username = username 88 | else: 89 | #task is done 90 | tile.checkin = 1 91 | tile.change = True 92 | tile.checkout = False 93 | session.add(tile) 94 | session.flush() 95 | 96 | # reset tile values 97 | tile.username = None 98 | tile.change = False 99 | tile.comment = None 100 | session.add(tile) 101 | return dict(success=True, tile=dict(x=tile.x, y=tile.y, z=tile.zoom)) 102 | 103 | @view_config(route_name='task_unlock', permission='job', renderer='json') 104 | def unlock(request): 105 | job_id = request.matchdict['job'] 106 | x = request.matchdict['x'] 107 | y = request.matchdict['y'] 108 | zoom = request.matchdict['zoom'] 109 | session = DBSession() 110 | tile = session.query(Tile).get((x, y, zoom, job_id)) 111 | tile.comment = request.params['comment'] 112 | tile.username = None 113 | tile.checkout = False 114 | tile.change = False 115 | session.add(tile) 116 | return dict(success=True, tile=dict(x=tile.x, y=tile.y, z=tile.zoom)) 117 | 118 | @view_config(route_name='task_lock', permission='job', renderer="json") 119 | def lock(request): 120 | job_id = request.matchdict['job'] 121 | session = DBSession() 122 | username = authenticated_userid(request) 123 | user = session.query(User).get(username) 124 | job = session.query(Job).get(job_id) 125 | 126 | x = request.matchdict['x'] 127 | y = request.matchdict['y'] 128 | zoom = request.matchdict['zoom'] 129 | tile = session.query(Tile).get((x, y, zoom, job_id)) 130 | 131 | # task is already checked out by someone else 132 | if tile.checkout is True and tile.username != user: 133 | msg = 'You cannot lock this task. Someone else is already working on it.' 134 | return dict(error_msg=msg) 135 | 136 | # check if user has no task he's currently working on 137 | filter = and_(Tile.username==username, Tile.checkout==True, Tile.job_id==job_id) 138 | tiles_current = session.query(Tile).filter(filter).all() 139 | if len(tiles_current) > 0 and tile.user != user: 140 | msg = 'You already have a task to work on. Finish it before you can accept a new one.' 141 | return dict(error_msg=msg) 142 | 143 | try: 144 | tile.username = username 145 | tile.checkout = True 146 | tile.change = False 147 | tile.comment = None 148 | session.add(tile) 149 | return dict(success=True, tile=dict(x=tile.x, y=tile.y, z=tile.zoom)) 150 | except: 151 | if int(checkin) == 1: 152 | msg = 'Sorry. No task available to validate.' 153 | else: 154 | msg = 'Sorry. No task available to take.' 155 | return dict(job=job, error_msg=msg) 156 | 157 | @view_config(route_name='task_take_random', permission='job', renderer="json") 158 | def take_random(request): 159 | job_id = request.matchdict['job'] 160 | if "checkin" in request.matchdict: 161 | checkin = request.matchdict['checkin'] 162 | else: 163 | checkin = None 164 | session = DBSession() 165 | username = authenticated_userid(request) 166 | user = session.query(User).get(username) 167 | job = session.query(Job).get(job_id) 168 | 169 | filter = and_(Tile.checkin==checkin, Tile.job_id==job_id) 170 | tiles = session.query(Tile).filter(filter).all() 171 | # take random tile 172 | if checkin is not None: 173 | # get the tile the user worked on previously 174 | filter = and_(TileHistory.username==username, TileHistory.job_id==job_id) 175 | p = session.query(TileHistory).filter(filter).order_by(TileHistory.update.desc()).limit(4).all() 176 | tile = None 177 | if p is not None and len(p) > 0: 178 | p = p[len(p) -1] 179 | neighbours = [ 180 | (p.x - 1, p.y - 1), (p.x - 1, p.y), (p.x - 1, p.y + 1), 181 | (p.x, p.y - 1), (p.x, p.y + 1), 182 | (p.x + 1, p.y - 1), (p.x + 1, p.y), (p.x + 1, p.y + 1)] 183 | for t in tiles: 184 | if (t.x, t.y) in neighbours: 185 | tile = t 186 | break 187 | if tile is None: 188 | tile = tiles[random.randrange(0, len(tiles))] 189 | 190 | return dict(success=True, tile=dict(x=tile.x, y=tile.y, z=tile.zoom)) 191 | 192 | 193 | @view_config(route_name='task_split', permission='job', renderer="geojson") 194 | def split_tile(request): 195 | """ 196 | Split the tile and copy history of the parent tile 197 | """ 198 | job_id = request.matchdict['job'] 199 | x = request.matchdict['x'] 200 | y = request.matchdict['y'] 201 | zoom = request.matchdict['zoom'] 202 | session = DBSession() 203 | job = session.query(Job).get(job_id) 204 | tile = session.query(Tile).get((x, y, zoom, job_id)) 205 | session.delete(tile) 206 | 207 | # reference tile history 208 | tileHistory = ( 209 | session.query(TileHistory) 210 | .filter_by(x=x, y=y, zoom=zoom, job_id=job_id) 211 | .order_by(TileHistory.update) 212 | .all() 213 | ) 214 | 215 | new_tiles = [] 216 | t = [] 217 | for i in range(0, 2): 218 | for j in range(0, 2): 219 | # add new tile 220 | X = int(x) * 2 + i 221 | Y = int(y) * 2 + j 222 | Zoom = int(zoom) + 1 223 | newTile = Tile(X, Y, Zoom) 224 | newTile.job = job 225 | 226 | for idx, historyRecord in enumerate(tileHistory): 227 | # copy tileHistory... use negative versions to prevent unique 228 | # key conflicts, and enable filtering (exclusion) when 229 | # generating stats 230 | newTileHistory = TileHistory( 231 | x=newTile.x, 232 | y=newTile.y, 233 | zoom=newTile.zoom, 234 | username=historyRecord.username, 235 | update=historyRecord.update, 236 | checkout=historyRecord.checkout, 237 | checkin=historyRecord.checkin, 238 | change=historyRecord.change, 239 | comment=historyRecord.comment, 240 | job_id=job.id, 241 | version=-idx 242 | ) 243 | session.add(newTileHistory) 244 | 245 | t.append(newTile) 246 | for tile in t: 247 | new_tiles.append( 248 | Feature( 249 | geometry=tile.to_polygon(), 250 | id=str(tile.x) + '-' + str(tile.y) + '-' + str(tile.zoom))) 251 | return dict( 252 | success=True, split_id="-".join([x, y, zoom]), 253 | new_tiles=FeatureCollection(new_tiles) 254 | ) 255 | 256 | 257 | @view_config(route_name="task_export_osm", renderer="task.osm.mako") 258 | def task_export_osm(request): 259 | job_id = request.matchdict['job'] 260 | x = request.matchdict['x'] 261 | y = request.matchdict['y'] 262 | zoom = request.matchdict['zoom'] 263 | session = DBSession() 264 | tile = session.query(Tile).get((x, y, zoom, job_id)) 265 | return dict(polygon=tile.to_polygon(4326)) 266 | 267 | @view_config(route_name="task_export_gpx", renderer="task.gpx.mako") 268 | def task_export_gpx(request): 269 | job_id = request.matchdict['job'] 270 | x = request.matchdict['x'] 271 | y = request.matchdict['y'] 272 | zoom = request.matchdict['zoom'] 273 | session = DBSession() 274 | tile = session.query(Tile).get((x, y, zoom, job_id)) 275 | return dict(polygon=tile.to_polygon(4326), job_id=job_id) 276 | 277 | def get_locked_task(job_id, username): 278 | session = DBSession() 279 | try: 280 | filter = and_(Tile.username==username, Tile.checkout==True, Tile.job_id==job_id) 281 | return session.query(Tile).filter(filter).one() 282 | except NoResultFound, e: 283 | return None 284 | -------------------------------------------------------------------------------- /OSMTM/views/views.py: -------------------------------------------------------------------------------- 1 | import urlparse 2 | from xml.etree import ElementTree 3 | from pyramid.httpexceptions import HTTPFound, HTTPBadGateway, HTTPBadRequest 4 | from pyramid.view import view_config 5 | from pyramid.url import route_url 6 | from pyramid.renderers import render_to_response 7 | 8 | from OSMTM.models import DBSession 9 | from OSMTM.models import Job 10 | from OSMTM.models import User 11 | from OSMTM.models import Tile 12 | from OSMTM.models import TileHistory 13 | from OSMTM.models import Tag 14 | 15 | import oauth2 as oauth 16 | 17 | from pyramid.security import remember, forget, authenticated_userid 18 | 19 | from json import dumps 20 | from markdown import markdown 21 | from OSMTM.utils import timesince 22 | from datetime import datetime, timedelta 23 | from sqlalchemy import desc, distinct, func 24 | from sqlalchemy.sql.expression import and_ 25 | 26 | from OSMTM.utils import transform_900913_to_4326 27 | 28 | import logging 29 | log = logging.getLogger(__file__) 30 | 31 | # 32 | # Constants 33 | # 34 | 35 | # our oauth key and secret (we're the consumer in the oauth protocol) 36 | # consumer key and secret created by Kate Chapman 37 | CONSUMER_KEY = 'BOFkVgLDXTSMP6VHfiX8MQ' 38 | CONSUMER_SECRET = '4o4uLSqLWMciG2fE2zGncLcdewPNi9wU1To51Iz2E' 39 | 40 | # OSM oauth URLs 41 | BASE_URL = 'http://www.openstreetmap.org/oauth' 42 | REQUEST_TOKEN_URL = '%s/request_token' % BASE_URL 43 | ACCESS_TOKEN_URL = '%s/access_token' % BASE_URL 44 | AUTHORIZE_URL = '%s/authorize' % BASE_URL 45 | 46 | # OSM user details URL 47 | USER_DETAILS_URL = 'http://api.openstreetmap.org/api/0.6/user/details' 48 | 49 | # an oauth consumer instance using our key and secret 50 | consumer = oauth.Consumer(CONSUMER_KEY, CONSUMER_SECRET) 51 | 52 | @view_config(route_name='login') 53 | def login(request): 54 | # get the request token 55 | client = oauth.Client(consumer) 56 | oauth_callback_url = request.route_url('oauth_callback') 57 | url = "%s?oauth_callback=%s" % (REQUEST_TOKEN_URL, oauth_callback_url) 58 | resp, content = client.request(url, "GET") 59 | if resp['status'] != '200': 60 | return HTTPBadGateway('The OSM authentication server didn\'t respond correctly') 61 | request_token = dict(urlparse.parse_qsl(content)) 62 | # store the request token in the session, we'll need in the callback 63 | session = request.session 64 | session['request_token'] = request_token 65 | session['came_from'] = request.params.get('came_from') 66 | session.save() 67 | redirect_url = "%s?oauth_token=%s" % \ 68 | (AUTHORIZE_URL, request_token['oauth_token']) 69 | return HTTPFound(location=redirect_url) 70 | 71 | @view_config(route_name='oauth_callback') 72 | def oauth_callback(request): 73 | # the request token we have in the user session should be the same 74 | # as the one passed to the callback 75 | session = request.session 76 | request_token = session.get('request_token') 77 | if request.params.get('oauth_token') != request_token['oauth_token']: 78 | return HTTPBadRequest('Tokens don\'t match') 79 | # get the access token 80 | token = oauth.Token(request_token['oauth_token'], 81 | request_token['oauth_token_secret']) 82 | verifier = request.params.get('oauth_verifier') 83 | token.set_verifier(verifier) 84 | client = oauth.Client(consumer, token) 85 | resp, content = client.request(ACCESS_TOKEN_URL, "POST") 86 | access_token = dict(urlparse.parse_qsl(content)) 87 | token = access_token['oauth_token'] 88 | token_secret = access_token['oauth_token_secret'] 89 | # get the user details, finally 90 | token = oauth.Token(token, token_secret) 91 | client = oauth.Client(consumer, token) 92 | resp, content = client.request(USER_DETAILS_URL, "GET") 93 | user_elt = ElementTree.XML(content).find('user') 94 | # save the user's "display name" in the session 95 | if 'display_name' in user_elt.attrib: 96 | username = user_elt.attrib['display_name'] 97 | db_session = DBSession() 98 | if db_session.query(User).get(username) is None: 99 | db_session.add(User(username)) 100 | db_session.flush() 101 | headers = remember(request, username, max_age=20*7*24*60*60) 102 | 103 | # and redirect to the main page 104 | return HTTPFound(location=session.get('came_from'), headers=headers) 105 | 106 | @view_config(route_name='logout') 107 | def logout(request): 108 | headers = forget(request) 109 | return HTTPFound(location=request.route_url('home'), headers=headers) 110 | 111 | @view_config(route_name='home', renderer='home.mako', permission='edit') 112 | def home(request): 113 | session = DBSession() 114 | username = authenticated_userid(request) 115 | user = session.query(User).get(username) 116 | jobs = session.query(Job).order_by(desc(Job.id)) 117 | if user is None: 118 | redirect = request.params.get("redirect", request.route_url("logout")) 119 | return HTTPFound(location=redirect) 120 | if not user.is_admin(): 121 | jobs = [job for job in jobs if not job.is_private and job.status == 1] + user.private_jobs 122 | tiles = session.query(Tile) \ 123 | .filter(Tile.username!=None) \ 124 | .group_by(Tile.username) 125 | # unlock expired tiles 126 | for tile in tiles: 127 | checkTask(tile) 128 | my_jobs = session.query(TileHistory) \ 129 | .filter(TileHistory.username==user.username) \ 130 | .group_by(TileHistory.job_id) 131 | my_jobs = [tile.job_id for tile in my_jobs] 132 | 133 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None 134 | 135 | def to_five(i): 136 | return int(round(i/5)) * 5 137 | 138 | def to_dict(job): 139 | centroid = job.get_centroid() 140 | filter = and_(Tile.job==job,Tile.checkout==True, Tile.username!=None) 141 | current_users = session.query(distinct(Tile.username)) \ 142 | .filter(filter).all() 143 | current_users = [u[0] for u in current_users] 144 | 145 | x, y = transform_900913_to_4326(centroid.x, centroid.y) 146 | left = (x + 180) * 120 / 360 - 1 147 | top = (-y + 90) * 60 / 180 - 1 148 | 149 | return dict( 150 | title=job.title, 151 | status=job.status, 152 | short_description=markdown(job.short_description), 153 | author=job.author, 154 | is_private=job.is_private, 155 | featured=job.featured, 156 | last_update=timesince(job.last_update), 157 | done=job.done, 158 | users=current_users, 159 | usersText="Currently working: %s" % ", ".join(current_users), 160 | url=request.route_url('job', job=job.id), 161 | feature_url=request.route_url('job_feature', job=job.id), 162 | archive_url=request.route_url('job_archive', job=job.id), 163 | publish_url=request.route_url('job_publish', job=job.id), 164 | edit_url=request.route_url('job_edit', job=job.id), 165 | tags=[tag.tag for tag in job.tags], 166 | is_mine=job.id in [_job for _job in my_jobs], 167 | lon=centroid.x, 168 | lat=centroid.y, 169 | left=left, 170 | top=top 171 | ) 172 | 173 | jobs = dumps([to_dict(job) for job in jobs], default=dthandler) 174 | 175 | return dict(jobs=jobs, 176 | user=user, 177 | admin=user.is_admin(), 178 | my_jobs=my_jobs) 179 | 180 | @view_config(route_name='about', renderer='about.mako') 181 | def about(request): 182 | return dict() 183 | 184 | @view_config(route_name='user', renderer='user.mako') 185 | def user(request): 186 | session = DBSession() 187 | profile_user = session.query(User).get(request.matchdict["id"]) 188 | jobs = user_job_info(profile_user.username) 189 | 190 | username = authenticated_userid(request) 191 | user = session.query(User).get(username) 192 | admin=user.is_admin() 193 | return dict(user=profile_user, jobs=jobs, admin=admin) 194 | 195 | @view_config(route_name='user_edit', renderer='user_edit.mako', permission='admin') 196 | def user_edit(request): 197 | session = DBSession() 198 | user = session.query(User).get(request.matchdict["id"]) 199 | return dict(user=user, admin=True) 200 | 201 | @view_config(route_name='user_update', permission='admin') 202 | def user_update(request): 203 | session = DBSession() 204 | user = session.query(User).get(request.matchdict["id"]) 205 | if 'form.submitted' in request.params: 206 | user.admin = True if 'admin' in request.params else False 207 | session.flush() 208 | #request.session.flash('Profile correctly updated!') 209 | return HTTPFound(location=request.route_url('user',id=user.username)) 210 | 211 | @view_config(route_name='user_add', permission='admin') 212 | def user_add(request): 213 | session = DBSession() 214 | username = request.params.get("username") 215 | if session.query(User).get(username) is None: 216 | session.add(User(username)) 217 | session.flush() 218 | return HTTPFound(location=request.route_url('user', id=username)) 219 | 220 | @view_config(route_name='users', renderer='users.mako', permission="edit") 221 | def users(request): 222 | session = DBSession() 223 | current_username = authenticated_userid(request) 224 | current_user = session.query(User).get(current_username) 225 | return dict(users = session.query(User), admin=current_user.is_admin()) 226 | 227 | def user_job_info(username): 228 | session = DBSession() 229 | 230 | """ get the tiles that changed """ 231 | filter = and_(TileHistory.change==True, TileHistory.checkin==1, 232 | TileHistory.username == username, TileHistory.job_id==Job.id) 233 | user_jobs = session.query(Job, func.count(TileHistory.username)) \ 234 | .filter(filter) \ 235 | .group_by(Job.id) \ 236 | .all() 237 | job_info = [{"job": job[0], "count": job[1]} for job in user_jobs] 238 | return job_info 239 | 240 | @view_config(route_name='tour', renderer='tour.mako') 241 | def tour(request): 242 | return dict() 243 | 244 | # the time delta after which the task is unlocked (in seconds) 245 | EXPIRATION_DURATION = timedelta(seconds=2 * 60 * 60) 246 | 247 | # unlock the tile if expired 248 | def checkTask(tile): 249 | session = DBSession() 250 | if tile.checkout is not False and tile.checkout is not None: 251 | if datetime.now() > tile.update + EXPIRATION_DURATION: 252 | tile.username = None 253 | tile.checkout = False 254 | tile.update = datetime.now() 255 | session.add(tile) 256 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Deprecated 2 | ========== 3 | 4 | **This repo is now deprecated in favor of** osm-tasking-manager2_ 5 | 6 | .. _osm-tasking-manager2: https://github.com/hotosm/osm-tasking-manager2 7 | 8 | 9 | 10 | 11 | 12 | OpenStreetMap Tasking Manager 13 | ============================= 14 | 15 | About 16 | ----- 17 | 18 | OSMTM enables collaborative work on specific areas in OpenStreetMap by defining 19 | clear workflows to be achieved and by breaking tasks down into pieces. 20 | 21 | The application is written in Python using the Pylons framework. 22 | 23 | 24 | Dependencies 25 | ------------ 26 | 27 | OSMTM has a set of dependencies that you need to install first. 28 | 29 | On debian systems you can do:: 30 | 31 | sudo apt-get install build-essential protobuf-compiler libprotobuf-dev libgeos-dev python-dev 32 | 33 | On OS X you can do:: 34 | 35 | brew install protobuf geos 36 | 37 | 38 | Installation 39 | ------------ 40 | 41 | First clone the git repository:: 42 | 43 | git clone git://github.com/hotosm/osm-tasking-manager.git 44 | 45 | Update and load the submodules:: 46 | 47 | cd osm-tasking-manager 48 | git submodule update --init 49 | 50 | Installing OSMTM in a Virtual Python environment is recommended. 51 | 52 | To create a virtual Python environment:: 53 | 54 | wget http://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.8.tar.gz 55 | tar xvzf virtualenv-1.8.tar.gz 56 | python virtualenv-1.8/virtualenv.py --distribute --no-site-packages env 57 | rm -rf distribute-0.6.28.tar.gz 58 | rm -rf virtualenv-1.8* 59 | source env/bin/activate 60 | 61 | To install OSMTM from source (the only option at this point) in the virtual 62 | Python environment execute the ``setup.py`` script:: 63 | 64 | python setup.py install 65 | 66 | Edit the development.ini file and change the ``admin_user`` variable to match 67 | your OSM username. 68 | 69 | 70 | Run OSMTM 71 | --------- 72 | 73 | To run OSMTM the easiest is to use ``pserve``:: 74 | 75 | pserve --reload development.ini 76 | 77 | 78 | Installation as a mod_wsgi Application 79 | -------------------------------------- 80 | 81 | Edit the production.ini file and change the ``admin_user`` variable to match 82 | your OSM username. 83 | 84 | Install and enable mod_wsgi module in Apache:: 85 | 86 | sudo apt-get install libapache2-mod-wsgi 87 | 88 | Create a new Apache config file with the following:: 89 | 90 | # Use only 1 Python sub-interpreter. Multiple sub-interpreters 91 | # play badly with C extensions. 92 | WSGIPassAuthorization On 93 | WSGIDaemonProcess OSMTM_process user=ubuntu group=ubuntu processes=1 \ 94 | threads=4 \ 95 | python-path=/home/ubuntu/osm-tasking-manager/env/lib/python2.7/site-packages 96 | WSGIScriptAlias /OSMTM /home/ubuntu/osm-tasking-manager/env/OSMTM.wsgi 97 | WSGIRestrictStdin Off 98 | 99 | 100 | WSGIProcessGroup OSMTM_process 101 | WSGIApplicationGroup %{GLOBAL} 102 | 103 | 104 | You may need to adpat the user, group and paths values. 105 | 106 | Create a new `OSMTM.wsgi` in your virtual env directory with the following:: 107 | 108 | import sys 109 | sys.stdout = sys.stderr 110 | 111 | from pyramid.paster import get_app 112 | application = get_app( 113 | '/home/ubuntu/osm-tasking-manager/production.ini', 'main') 114 | 115 | You can then test config and restart Apache. 116 | Your application should be available at http://host.domain/OSMTM 117 | 118 | 119 | You may also need to enable CORS. See http://enable-cors.org. 120 | Add the following to your virtual hosts:: 121 | 122 | Header set Access-Control-Allow-Origin "*" 123 | 124 | Styles 125 | ------ 126 | 127 | The CSS stylesheet are compiled using less. Launch the following command as 128 | soon as you change the css:: 129 | 130 | lessc OSMTM/static/css/main.less > OSMTM/static/css/main.less.min.css 131 | 132 | Run Tests 133 | --------- 134 | 135 | To ensure your build is working properly run the tests (in active virtual env):: 136 | 137 | nosetests 138 | 139 | Upgrade notes 140 | ------------- 141 | 142 | Database versions are now managed using Alembic. 143 | The following commands should help upgrading the database. 144 | 145 | *Don't forget to make copies of your db file before running any upgrade.*:: 146 | 147 | alembic upgrade head 148 | 149 | Note: Please contact the maintainer if you encounter problems. 150 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | OSMTM README 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | sqlalchemy.url = sqlite:///%(here)s/OSMTM.db 15 | 16 | # Logging configuration 17 | [loggers] 18 | keys = root,sqlalchemy,alembic 19 | 20 | [handlers] 21 | keys = console 22 | 23 | [formatters] 24 | keys = generic 25 | 26 | [logger_root] 27 | level = WARN 28 | handlers = console 29 | qualname = 30 | 31 | [logger_sqlalchemy] 32 | level = WARN 33 | handlers = 34 | qualname = sqlalchemy.engine 35 | 36 | [logger_alembic] 37 | level = INFO 38 | handlers = 39 | qualname = alembic 40 | 41 | [handler_console] 42 | class = StreamHandler 43 | args = (sys.stderr,) 44 | level = NOTSET 45 | formatter = generic 46 | 47 | [formatter_generic] 48 | format = %(levelname)-5.5s [%(name)s] %(message)s 49 | datefmt = %H:%M:%S 50 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from OSMTM.models import Base 19 | target_metadata = Base.metadata 20 | 21 | # other values from the config, defined by the needs of env.py, 22 | # can be acquired: 23 | # my_important_option = config.get_main_option("my_important_option") 24 | # ... etc. 25 | 26 | def run_migrations_offline(): 27 | """Run migrations in 'offline' mode. 28 | 29 | This configures the context with just a URL 30 | and not an Engine, though an Engine is acceptable 31 | here as well. By skipping the Engine creation 32 | we don't even need a DBAPI to be available. 33 | 34 | Calls to context.execute() here emit the given string to the 35 | script output. 36 | 37 | """ 38 | url = config.get_main_option("sqlalchemy.url") 39 | context.configure(url=url) 40 | 41 | with context.begin_transaction(): 42 | context.run_migrations() 43 | 44 | def run_migrations_online(): 45 | """Run migrations in 'online' mode. 46 | 47 | In this scenario we need to create an Engine 48 | and associate a connection with the context. 49 | 50 | """ 51 | engine = engine_from_config( 52 | config.get_section(config.config_ini_section), 53 | prefix='sqlalchemy.', 54 | poolclass=pool.NullPool) 55 | 56 | connection = engine.connect() 57 | context.configure( 58 | connection=connection, 59 | target_metadata=target_metadata 60 | ) 61 | 62 | try: 63 | with context.begin_transaction(): 64 | context.run_migrations() 65 | finally: 66 | connection.close() 67 | 68 | if context.is_offline_mode(): 69 | run_migrations_offline() 70 | else: 71 | run_migrations_online() 72 | 73 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /alembic/versions/173a7003596f_added_author_column.py: -------------------------------------------------------------------------------- 1 | """Added author column 2 | 3 | Revision ID: 173a7003596f 4 | Revises: 4fbba110ab8b 5 | Create Date: 2012-11-11 17:03:32.593702 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '173a7003596f' 11 | down_revision = '4fbba110ab8b' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('jobs', sa.Column('author', sa.Unicode(), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('jobs', 'author') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /alembic/versions/183cfaae4493_adding_done_and_upda.py: -------------------------------------------------------------------------------- 1 | """Adding done and last_update columns 2 | 3 | Revision ID: 183cfaae4493 4 | Revises: None 5 | Create Date: 2012-10-25 08:11:03.811958 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '183cfaae4493' 11 | down_revision = None 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('jobs', sa.Column('done', sa.Integer(), nullable=True)) 20 | op.add_column('jobs', sa.Column('last_update', sa.DateTime(), nullable=True)) 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('jobs', 'last_update') 27 | op.drop_column('jobs', 'done') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /alembic/versions/2edcc95ff6f7_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2edcc95ff6f7 4 | Revises: 55009f9d3296 5 | Create Date: 2013-03-26 16:22:55.243858 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2edcc95ff6f7' 11 | down_revision = '55009f9d3296' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | pass 19 | 20 | 21 | def downgrade(): 22 | pass 23 | -------------------------------------------------------------------------------- /alembic/versions/2faee55a9ed8_upgrade_after_workfl.py: -------------------------------------------------------------------------------- 1 | """Upgrade after workflow branch merge 2 | 3 | Revision ID: 2faee55a9ed8 4 | Revises: 5229d2fd908d 5 | Create Date: 2014-02-09 16:02:41.025040 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2faee55a9ed8' 11 | down_revision = '5229d2fd908d' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy import and_ 16 | 17 | # we build a quick link for the current connection of alembic 18 | connection = op.get_bind() 19 | 20 | def upgrade(): 21 | """ this script is made to upgrade data from 9 -> 10 """ 22 | from sqlalchemy.engine import create_engine 23 | engine = create_engine('sqlite:///OSMTM.db') 24 | connection = engine.connect() 25 | 26 | from OSMTM.models import TileHistory, Tile, Job 27 | from OSMTM.history_meta import VersionedListener 28 | from sqlalchemy import orm 29 | from sqlalchemy.sql.expression import and_ 30 | 31 | sm = orm.sessionmaker(bind=engine, autoflush=True, autocommit=False, 32 | expire_on_commit=True, 33 | extension=[VersionedListener()]) 34 | session = orm.scoped_session(sm) 35 | 36 | jobs = session.query(Job).all() 37 | 38 | for job in jobs: 39 | 40 | print "job: %s" % job.id 41 | 42 | tiles = session.query(Tile).filter(and_(Tile.change==True, Tile.job_id==job.id)) 43 | for tile in tiles: 44 | tile.change = False 45 | tile.username = None 46 | tile.comment = None 47 | session.add(tile) 48 | 49 | session.commit() 50 | 51 | pass 52 | 53 | 54 | def downgrade(): 55 | pass 56 | -------------------------------------------------------------------------------- /alembic/versions/41cd7451a8d7_adding_offset_fields.py: -------------------------------------------------------------------------------- 1 | """Adding offset fields 2 | 3 | Revision ID: 41cd7451a8d7 4 | Revises: 2faee55a9ed8 5 | Create Date: 2014-03-26 15:04:22.142334 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '41cd7451a8d7' 11 | down_revision = '2faee55a9ed8' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobs', sa.Column('imagery_offset_x', sa.Float(), default=0)) 19 | op.add_column('jobs', sa.Column('imagery_offset_y', sa.Float(), default=0)) 20 | pass 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('jobs', 'imagery_offset_x') 25 | op.drop_column('jobs', 'imagery_offset_y') 26 | pass 27 | -------------------------------------------------------------------------------- /alembic/versions/4fbba110ab8b_adding_index_for_til.py: -------------------------------------------------------------------------------- 1 | """Adding index for tiles.job_id 2 | 3 | Revision ID: 4fbba110ab8b 4 | Revises: 183cfaae4493 5 | Create Date: 2012-10-25 22:12:38.516115 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4fbba110ab8b' 11 | down_revision = '183cfaae4493' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.create_index('tiles_job_id_ndx', 'tiles', ['job_id']) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_index('tiles_job_id_ndx') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /alembic/versions/5229d2fd908d_adding_task_extra_co.py: -------------------------------------------------------------------------------- 1 | """Adding task_extra column 2 | 3 | Revision ID: 5229d2fd908d 4 | Revises: 2edcc95ff6f7 5 | Create Date: 2013-03-26 16:28:22.364216 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5229d2fd908d' 11 | down_revision = '2edcc95ff6f7' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('jobs', sa.Column('task_extra', sa.Unicode(), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('jobs', 'task_extra') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /alembic/versions/55009f9d3296_added_licenses_table.py: -------------------------------------------------------------------------------- 1 | """Added licenses table 2 | 3 | Revision ID: 55009f9d3296 4 | Revises: 173a7003596f 5 | Create Date: 2012-12-20 21:58:33.616095 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '55009f9d3296' 11 | down_revision = '173a7003596f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy import Unicode, Integer 16 | from sqlalchemy.sql import table, column 17 | 18 | 19 | def upgrade(): 20 | ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('licenses', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.Unicode(), nullable=True), 24 | sa.Column('description', sa.Unicode(), nullable=True), 25 | sa.Column('plain_text', sa.Unicode(), nullable=True), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | op.add_column('jobs', sa.Column('license_id', sa.Integer(), nullable=True)) 29 | 30 | op.create_table('users_licenses', 31 | sa.Column('user', sa.Integer(), nullable=True), 32 | sa.Column('license', sa.Integer(), nullable=True), 33 | sa.ForeignKeyConstraint(['license'], ['licenses.id'], ), 34 | sa.ForeignKeyConstraint(['user'], ['users.username'], ), 35 | sa.PrimaryKeyConstraint() 36 | ) 37 | # cannot drop column with SQLite 38 | #op.drop_column('jobs', u'requires_nextview') 39 | ### end Alembic commands ### 40 | 41 | licenses = table('licenses', 42 | column('name'), 43 | column('description') 44 | ) 45 | op.execute("INSERT INTO licenses (name, description, plain_text)\ 46 | VALUES ('NextView', 'This data is licensed for use by the US Government (USG) under the NextView (NV) license and copyrighted by Digital Globe or GeoEye. The NV license allows the USG to share the imagery and Literal Imagery Derived Products (LIDP) with entities outside the USG when that entity is working directly with the USG, for the USG, or in a manner that is directly beneficial to the USG. The party receiving the data can only use the imagery or LIDP for the original purpose or only as otherwise agreed to by the USG. The party receiving the data cannot share the imagery or LIDP with a third party without express permission from the USG. At no time should this imagery or LIDP be used for other than USG-related purposes and must not be used for commercial gain. The copyright information should be maintained at all times. Your acceptance of these license terms is implied by your use.', 'In other words, you may only use NextView imagery linked from this site for digitizing OpenStreetMap data for humanitarian purposes.')") 47 | op.execute("UPDATE jobs SET license_id = 1 WHERE requires_nextview = 1") 48 | op.execute("INSERT INTO users_licenses (user, license) SELECT username, 1 FROM users WHERE accepted_nextview = 1") 49 | -------------------------------------------------------------------------------- /development.ini: -------------------------------------------------------------------------------- 1 | [app:OSMTM] 2 | use = egg:OSMTM 3 | reload_templates = true 4 | debug_authorization = false 5 | debug_notfound = false 6 | debug_routematch = false 7 | debug_templates = true 8 | default_locale_name = en 9 | sqlalchemy.url = sqlite:///%(here)s/OSMTM.db 10 | session.type = file 11 | session.data_dir = %(here)s/data/sessions/data 12 | session.lock_dir = %(here)s/data/sessions/lock 13 | session.key = OSMQA 14 | session.secret = ae02254a597c45ca3f3b952183ad5f846f9b5b9a 15 | admin_user = pgiraud 16 | 17 | fanstatic.bottom = true 18 | fanstatic.debug = true 19 | 20 | pyramid.includes = pyramid_debugtoolbar 21 | 22 | [pipeline:main] 23 | pipeline = 24 | egg:WebError#evalerror 25 | tm 26 | OSMTM 27 | 28 | [filter:tm] 29 | use = egg:repoze.tm2#tm 30 | commit_veto = repoze.tm:default_commit_veto 31 | 32 | [server:main] 33 | use = egg:Paste#http 34 | host = 0.0.0.0 35 | port = 6543 36 | 37 | # Begin logging configuration 38 | 39 | [loggers] 40 | keys = root, OSMTM, sqlalchemy 41 | 42 | [handlers] 43 | keys = console 44 | 45 | [formatters] 46 | keys = generic 47 | 48 | [logger_root] 49 | level = INFO 50 | handlers = console 51 | 52 | [logger_OSMTM] 53 | level = DEBUG 54 | handlers = 55 | qualname = OSMTM 56 | 57 | [logger_sqlalchemy] 58 | level = WARN 59 | handlers = 60 | qualname = sqlalchemy.engine 61 | # "level = INFO" logs SQL queries. 62 | # "level = DEBUG" logs SQL queries and results. 63 | # "level = WARN" logs neither. (Recommended for production systems.) 64 | 65 | [handler_console] 66 | class = StreamHandler 67 | args = (sys.stderr,) 68 | level = NOTSET 69 | formatter = generic 70 | 71 | [formatter_generic] 72 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 73 | 74 | # End logging configuration 75 | -------------------------------------------------------------------------------- /migration/README: -------------------------------------------------------------------------------- 1 | This is a database migration repository. 2 | 3 | More information at 4 | http://code.google.com/p/sqlalchemy-migrate/ 5 | -------------------------------------------------------------------------------- /migration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/migration/__init__.py -------------------------------------------------------------------------------- /migration/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from migrate.versioning.shell import main 3 | main(debug='False') 4 | -------------------------------------------------------------------------------- /migration/migrate.cfg: -------------------------------------------------------------------------------- 1 | [db_settings] 2 | # Used to identify which repository this database is versioned under. 3 | # You can use the name of your project. 4 | repository_id=OSMTM migration 5 | 6 | # The name of the database table used to track the schema version. 7 | # This name shouldn't already be used by your project. 8 | # If this is changed once a database is under version control, you'll need to 9 | # change the table name in each database too. 10 | version_table=migrate_version 11 | 12 | # When committing a change script, Migrate will attempt to generate the 13 | # sql for all supported databases; normally, if one of them fails - probably 14 | # because you don't have that database installed - it is ignored and the 15 | # commit continues, perhaps ending successfully. 16 | # Databases in this list MUST compile successfully during a commit, or the 17 | # entire commit will fail. List the databases your application will actually 18 | # be using to ensure your updates to that database work properly. 19 | # This must be a list; example: ['postgres','sqlite'] 20 | required_dbs=[] 21 | -------------------------------------------------------------------------------- /migration/versions/001_Adding_next_view_related_table_and_columns.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | from OSMTM.models import Job 4 | 5 | 6 | 7 | def upgrade(migrate_engine): 8 | meta = MetaData() 9 | meta.bind = migrate_engine 10 | 11 | job_whitelists = Table('job_whitelists', meta, 12 | Column('job_id', Integer()), 13 | Column('user_id', String()), 14 | ) 15 | 16 | jobs = Table('jobs', meta, 17 | Column('id', Integer(), primary_key=True, nullable=False), 18 | Column('title', String()), 19 | Column('description', String()), 20 | Column('geometry', String()), 21 | Column('workflow', String()), 22 | Column('zoom', Integer()), 23 | ) 24 | 25 | users = Table('users', meta, 26 | Column('username', String(), primary_key=True, nullable=False), 27 | Column('role', Integer()), 28 | ) 29 | 30 | job_whitelists.create() 31 | col = Column('imagery', String) 32 | col.create(jobs) 33 | col = Column('is_private', Boolean) 34 | col.create(jobs) 35 | col = Column('requires_nextview', Boolean) 36 | col.create(jobs) 37 | col = Column('accepted_nextview', Boolean) 38 | col.create(users) 39 | pass 40 | 41 | def downgrade(migrate_engine): 42 | meta = MetaData() 43 | meta.bind = migrate_engine 44 | 45 | job_whitelists = Table('job_whitelists', meta, 46 | Column('job_id', Integer()), 47 | Column('user_id', String()), 48 | ) 49 | 50 | jobs = Table('jobs', meta, 51 | Column('id', Integer(), primary_key=True, nullable=False), 52 | Column('title', String()), 53 | Column('description', String()), 54 | Column('geometry', String()), 55 | Column('workflow', String()), 56 | Column('imagery', String()), 57 | Column('zoom', Integer()), 58 | Column('is_private', Integer()), 59 | Column('requires_nextview', Integer()), 60 | ) 61 | 62 | users = Table('users', meta, 63 | Column('username', String(), primary_key=True, nullable=False), 64 | Column('role', Integer()), 65 | Column('accepted_nextview', Integer()), 66 | ) 67 | 68 | job_whitelists.drop() 69 | jobs.c.imagery.drop() 70 | jobs.c.is_private.drop() 71 | jobs.c.requires_nextview.drop() 72 | users.c.accepted_nextview.drop() 73 | pass 74 | -------------------------------------------------------------------------------- /migration/versions/002_Remove_role_column,_add_admin_column.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | def upgrade(migrate_engine): 5 | meta = MetaData() 6 | meta.bind = migrate_engine 7 | users = Table('users', meta, 8 | Column('username', String(), primary_key=True, nullable=False), 9 | Column('role', Integer()), 10 | Column('accepted_nextview', Boolean()), 11 | ) 12 | col = Column('admin', Boolean) 13 | col.create(users) 14 | col = Column('role', Integer) 15 | users.c.role.drop() 16 | pass 17 | 18 | def downgrade(migrate_engine): 19 | meta = MetaData() 20 | meta.bind = migrate_engine 21 | users = Table('users', meta, 22 | Column('username', String(), primary_key=True, nullable=False), 23 | Column('admin', Integer()), 24 | Column('accepted_nextview', Boolean()), 25 | ) 26 | users.c.admin.drop() 27 | col = Column('role', Integer) 28 | col.create(users) 29 | pass 30 | -------------------------------------------------------------------------------- /migration/versions/003_Add_short_description_column.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | def upgrade(migrate_engine): 5 | meta = MetaData() 6 | meta.bind = migrate_engine 7 | jobs = Table('jobs', meta, 8 | Column('id', Integer(), primary_key=True, nullable=False), 9 | Column('title', String()), 10 | Column('description', String()), 11 | Column('geometry', String()), 12 | Column('workflow', String()), 13 | Column('imagery', String()), 14 | Column('zoom', Integer()), 15 | Column('is_private', Integer()), 16 | Column('requires_nextview', Integer()), 17 | ) 18 | col = Column('short_description', String(), default='') 19 | col.create(jobs) 20 | pass 21 | 22 | def downgrade(migrate_engine): 23 | meta = MetaData() 24 | meta.bind = migrate_engine 25 | jobs = Table('jobs', meta, 26 | Column('id', Integer(), primary_key=True, nullable=False), 27 | Column('title', String()), 28 | Column('description', String()), 29 | Column('geometry', String()), 30 | Column('workflow', String()), 31 | Column('imagery', String()), 32 | Column('zoom', Integer()), 33 | Column('is_private', Integer()), 34 | Column('requires_nextview', Integer()), 35 | Column('short_description', String()) 36 | ) 37 | jobs.c.short_description.drop() 38 | pass 39 | -------------------------------------------------------------------------------- /migration/versions/004_Change_checkout_column_to_update.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | def upgrade(migrate_engine): 5 | meta = MetaData() 6 | meta.bind = migrate_engine 7 | tiles = Table('tiles', meta, 8 | Column('x', Integer, primary_key=True), 9 | Column('y', Integer, primary_key=True), 10 | Column('job_id', Integer, primary_key=True), 11 | Column('username', Unicode), 12 | Column('checkout', DateTime), 13 | Column('checkin', Integer), 14 | Column('comment', Unicode), 15 | Column('version', Integer), 16 | ) 17 | tiles.c.checkout.alter(name='update') 18 | 19 | tiles_h = Table('tiles_history', meta, 20 | Column('x', Integer, primary_key=True), 21 | Column('y', Integer, primary_key=True), 22 | Column('job_id', Integer, primary_key=True), 23 | Column('username', Unicode), 24 | Column('checkout', DateTime), 25 | Column('checkin', Integer), 26 | Column('comment', Unicode), 27 | Column('version', Integer, primary_key=True), 28 | ) 29 | tiles_h.c.checkout.alter(name='update') 30 | pass 31 | 32 | def downgrade(migrate_engine): 33 | meta = MetaData() 34 | meta.bind = migrate_engine 35 | tiles = Table('tiles', meta, 36 | Column('x', Integer, primary_key=True), 37 | Column('y', Integer, primary_key=True), 38 | Column('job_id', Integer, primary_key=True), 39 | Column('username', Unicode), 40 | Column('update', DateTime), 41 | Column('checkin', Integer), 42 | Column('comment', Unicode), 43 | Column('version', Integer), 44 | ) 45 | tiles.c['update'].alter(name='checkout') 46 | 47 | tiles_h = Table('tiles_history', meta, 48 | Column('x', Integer, primary_key=True), 49 | Column('y', Integer, primary_key=True), 50 | Column('job_id', Integer, primary_key=True), 51 | Column('username', Unicode), 52 | Column('update', DateTime), 53 | Column('checkin', Integer), 54 | Column('comment', Unicode), 55 | Column('version', Integer, primary_key=True), 56 | ) 57 | tiles_h.c['update'].alter(name='checkout') 58 | pass 59 | -------------------------------------------------------------------------------- /migration/versions/005_Add_status_column_in_jobs_table.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | def upgrade(migrate_engine): 5 | meta = MetaData() 6 | meta.bind = migrate_engine 7 | jobs = Table('jobs', meta, 8 | Column('id', Integer(), primary_key=True, nullable=False), 9 | Column('title', String()), 10 | Column('description', String()), 11 | Column('geometry', String()), 12 | Column('workflow', String()), 13 | Column('imagery', String()), 14 | Column('zoom', Integer()), 15 | Column('is_private', Integer()), 16 | Column('requires_nextview', Integer()), 17 | Column('short_description', String()) 18 | ) 19 | col = Column('status', Integer(), default=1) 20 | col.create(jobs) 21 | pass 22 | 23 | def downgrade(migrate_engine): 24 | meta = MetaData() 25 | meta.bind = migrate_engine 26 | jobs = Table('jobs', meta, 27 | Column('id', Integer(), primary_key=True, nullable=False), 28 | Column('title', String()), 29 | Column('description', String()), 30 | Column('geometry', String()), 31 | Column('workflow', String()), 32 | Column('imagery', String()), 33 | Column('zoom', Integer()), 34 | Column('is_private', Integer()), 35 | Column('requires_nextview', Integer()), 36 | Column('short_description', String()), 37 | Column('status', Integer()) 38 | ) 39 | jobs.c.status.drop() 40 | pass 41 | -------------------------------------------------------------------------------- /migration/versions/006_Add_tags.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | from OSMTM.models import * 4 | 5 | meta = MetaData() 6 | jobs = Table('jobs', meta, 7 | Column('id', Integer(), primary_key=True)) 8 | tags = Table('tags', meta, 9 | Column('tag', String(), primary_key=True, nullable=False), 10 | ) 11 | 12 | job_tags_table = Table('job_tags', meta, 13 | Column('job_id', Integer, ForeignKey('jobs.id')), 14 | Column('tag', Integer, ForeignKey('tags.tag')) 15 | ) 16 | 17 | def upgrade(migrate_engine): 18 | meta.bind = migrate_engine 19 | tags.create() 20 | job_tags_table.create() 21 | pass 22 | 23 | def downgrade(migrate_engine): 24 | meta.bind = migrate_engine 25 | tags.drop() 26 | job_tags_table.drop() 27 | pass 28 | -------------------------------------------------------------------------------- /migration/versions/007_Add_preset_column.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | from OSMTM.models import * 4 | 5 | meta = MetaData() 6 | 7 | def upgrade(migrate_engine): 8 | meta.bind = migrate_engine 9 | jobs = Table('jobs', meta, 10 | Column('id', Integer(), primary_key=True, nullable=False), 11 | Column('title', String()), 12 | Column('description', String()), 13 | Column('geometry', String()), 14 | Column('workflow', String()), 15 | Column('imagery', String()), 16 | Column('zoom', Integer()), 17 | Column('is_private', Integer()), 18 | Column('requires_nextview', Integer()), 19 | Column('short_description', String()), 20 | Column('status', Integer()), 21 | ) 22 | col = Column('josm_preset', String()) 23 | col.create(jobs) 24 | pass 25 | 26 | def downgrade(migrate_engine): 27 | meta.bind = migrate_engine 28 | jobs = Table('jobs', meta, 29 | Column('id', Integer(), primary_key=True, nullable=False), 30 | Column('title', String()), 31 | Column('description', String()), 32 | Column('geometry', String()), 33 | Column('workflow', String()), 34 | Column('imagery', String()), 35 | Column('zoom', Integer()), 36 | Column('is_private', Integer()), 37 | Column('requires_nextview', Integer()), 38 | Column('short_description', String()), 39 | Column('status', Integer()), 40 | Column('josm_preset', String()), 41 | ) 42 | jobs.c.josm_preset.drop() 43 | pass 44 | -------------------------------------------------------------------------------- /migration/versions/008_Adding_new_change_column.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | print "Also ensure that you correctly update your data using the script as well" 5 | 6 | def upgrade(migrate_engine): 7 | meta = MetaData() 8 | meta.bind = migrate_engine 9 | tiles = Table('tiles', meta, 10 | Column('x', Integer(), primary_key=True), 11 | Column('y', Integer(), primary_key=True), 12 | Column('job_id', Integer(), primary_key=True), 13 | Column('username', Unicode()), 14 | Column('update', DateTime()), 15 | Column('checkin', Integer()), 16 | Column('comment', Unicode()), 17 | Column('version', Integer()), 18 | ) 19 | 20 | tiles_h = Table('tiles_history', meta, 21 | Column('x', Integer(), primary_key=True), 22 | Column('y', Integer(), primary_key=True), 23 | Column('job_id', Integer(), primary_key=True), 24 | Column('username', Unicode()), 25 | Column('update', DateTime()), 26 | Column('checkin', Integer()), 27 | Column('comment', Unicode()), 28 | Column('version', Integer(), primary_key=True), 29 | ) 30 | col1 = Column('checkout', Boolean()) 31 | col1.create(tiles) 32 | col2 = Column('change', Boolean()) 33 | col2.create(tiles) 34 | col1 = Column('checkout', Boolean()) 35 | col1.create(tiles_h) 36 | col2 = Column('change', Boolean()) 37 | col2.create(tiles_h) 38 | pass 39 | 40 | def downgrade(migrate_engine): 41 | meta = MetaData() 42 | meta.bind = migrate_engine 43 | tiles = Table('tiles', meta, 44 | Column('x', Integer(), primary_key=True), 45 | Column('y', Integer(), primary_key=True), 46 | Column('job_id', Integer(), primary_key=True), 47 | Column('username', Unicode()), 48 | Column('update', DateTime()), 49 | Column('checkin', Integer()), 50 | Column('comment', Unicode()), 51 | Column('version', Integer()), 52 | Column('checkout', String()), 53 | Column('change', String()), 54 | ) 55 | 56 | tiles_h = Table('tiles_history', meta, 57 | Column('x', Integer(), primary_key=True), 58 | Column('y', Integer(), primary_key=True), 59 | Column('job_id', Integer(), primary_key=True), 60 | Column('username', Unicode()), 61 | Column('update', DateTime()), 62 | Column('checkin', Integer()), 63 | Column('comment', Unicode()), 64 | Column('version', Integer(), primary_key=True), 65 | Column('checkout', String()), 66 | Column('change', String()), 67 | ) 68 | tiles.c.checkout.drop() 69 | tiles.c.change.drop() 70 | tiles_h.c.checkout.drop() 71 | tiles_h.c.change.drop() 72 | pass 73 | -------------------------------------------------------------------------------- /migration/versions/009_Adding_featured_attribute.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | from OSMTM.models import * 4 | 5 | 6 | def upgrade(migrate_engine): 7 | meta = MetaData() 8 | meta.bind = migrate_engine 9 | jobs = Table('jobs', meta, 10 | Column('id', Integer(), primary_key=True, nullable=False), 11 | Column('title', String()), 12 | Column('description', String()), 13 | Column('geometry', String()), 14 | Column('workflow', String()), 15 | Column('imagery', String()), 16 | Column('zoom', Integer()), 17 | Column('is_private', Integer()), 18 | Column('requires_nextview', Integer()), 19 | Column('short_description', String()), 20 | Column('status', Integer()), 21 | Column('josm_preset', String()), 22 | ) 23 | col = Column('featured', Boolean()) 24 | col.create(jobs) 25 | pass 26 | 27 | 28 | def downgrade(migrate_engine): 29 | meta = MetaData() 30 | meta.bind = migrate_engine 31 | jobs = Table('jobs', meta, 32 | Column('id', Integer(), primary_key=True, nullable=False), 33 | Column('title', String()), 34 | Column('description', String()), 35 | Column('geometry', String()), 36 | Column('workflow', String()), 37 | Column('imagery', String()), 38 | Column('zoom', Integer()), 39 | Column('is_private', Integer()), 40 | Column('requires_nextview', Integer()), 41 | Column('short_description', String()), 42 | Column('status', Integer()), 43 | Column('josm_preset', String()), 44 | Column('featured', String()) 45 | ) 46 | jobs.c.featured.drop() 47 | pass 48 | -------------------------------------------------------------------------------- /migration/versions/010_Move_zoom_from_job_to_task.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | def upgrade(migrate_engine): 5 | meta = MetaData() 6 | meta.bind = migrate_engine 7 | 8 | tiles = Table('tiles', meta, 9 | Column('x', Integer(), primary_key=True), 10 | Column('y', Integer(), primary_key=True), 11 | Column('job_id', Integer(), primary_key=True), 12 | Column('username', Unicode()), 13 | Column('update', DateTime()), 14 | Column('checkin', Integer()), 15 | Column('comment', Unicode()), 16 | Column('version', Integer()), 17 | Column('checkout', String()), 18 | Column('change', String()), 19 | ) 20 | col = Column('zoom', Integer(), server_default="1") 21 | col.create(tiles) 22 | 23 | cons = PrimaryKeyConstraint(tiles.c.x, tiles.c.y, tiles.c.job_id, col) 24 | cons.create() 25 | 26 | tiles_h = Table('tiles_history', meta, 27 | Column('x', Integer(), primary_key=True), 28 | Column('y', Integer(), primary_key=True), 29 | Column('job_id', Integer(), primary_key=True), 30 | Column('username', Unicode()), 31 | Column('update', DateTime()), 32 | Column('checkin', Integer()), 33 | Column('comment', Unicode()), 34 | Column('version', Integer(), primary_key=True), 35 | Column('checkout', String()), 36 | Column('change', String()), 37 | ) 38 | col = Column('zoom', Integer(), server_default="1") 39 | col.create(tiles_h) 40 | 41 | cons = PrimaryKeyConstraint(tiles_h.c.x, tiles_h.c.y, tiles_h.c.job_id, col, tiles_h.c.version) 42 | cons.create() 43 | pass 44 | 45 | 46 | def downgrade(migrate_engine): 47 | # supporting downgrade is really a pain 48 | pass 49 | -------------------------------------------------------------------------------- /migration/versions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotosm/osm-tasking-manager/03474f4590f6f000d0b05895aff5298227e013c5/migration/versions/__init__.py -------------------------------------------------------------------------------- /migration/versions/data_upgrade_008.py: -------------------------------------------------------------------------------- 1 | """ this script is made to upgrade data from 7 -> 8 """ 2 | from sqlalchemy.engine import create_engine 3 | engine = create_engine('sqlite:///OSMTM.db') 4 | connection = engine.connect() 5 | 6 | from OSMTM.models import TileHistory, Tile 7 | from sqlalchemy import orm 8 | from sqlalchemy.sql.expression import and_ 9 | 10 | sm = orm.sessionmaker(bind=engine, autoflush=True, autocommit=False, 11 | expire_on_commit=True) 12 | session = orm.scoped_session(sm) 13 | 14 | tiles = session.query(Tile) \ 15 | .all() 16 | 17 | def compare_checkin(old, new): 18 | # task done 19 | if old == 0 and new == 1: 20 | return 1 21 | # task validated 22 | if old == 1 and new == 2: 23 | return 2 24 | # task invalidated 25 | if old == 1 and new == 0: 26 | return 3 27 | 28 | for tile in tiles: 29 | filter = and_(TileHistory.x==tile.x, TileHistory.y==tile.y, TileHistory.job_id==tile.job_id) 30 | tiles_history = session.query(TileHistory) \ 31 | .filter(filter) \ 32 | .order_by(TileHistory.version) \ 33 | .all() 34 | 35 | """ (re)initialize values """ 36 | username = None 37 | update = None 38 | last_history = None 39 | for ndx, i in enumerate(tiles_history): 40 | 41 | if i.username: 42 | i.checkout = True 43 | session.add(i) 44 | 45 | for ndx, i in enumerate(tiles_history): 46 | print "%s %s %s %s %s %s" % (i.job_id, i.x, i.y, i.username, i.checkin, i.version) 47 | 48 | if ndx > 0: 49 | if i.checkout: 50 | username = i.username 51 | update = i.update 52 | checkin = i.checkin 53 | 54 | status = compare_checkin(checkin, i.checkin) 55 | if status is not None: 56 | i.change = True 57 | i.username = username 58 | i.update = i.update if i.update != None else update 59 | else: 60 | i.comment = None 61 | 62 | last_history = i 63 | 64 | """ now compare with current tile status """ 65 | if last_history is not None: 66 | status = compare_checkin(last_history.checkin, tile.checkin) 67 | if status is not None: 68 | tile.change = True 69 | tile.username = username 70 | tile.update = tile.update if tile.update != None else update 71 | 72 | session.commit() 73 | 74 | """ 75 | tiles_history = session.query(TileHistory) \ 76 | .filter(TileHistory.job_id==job_id) \ 77 | .order_by(TileHistory.x, TileHistory.y) \ 78 | .all() 79 | user = None 80 | for ndx, i in enumerate(tiles_history): 81 | print "%s %s %s %s %s" % (str(i.x), str(i.y), i.username, i.checkin, i.version) 82 | 83 | if user is not None: 84 | status = compare_checkin(checkin, i.checkin) 85 | print "status %s " % status 86 | 87 | if i.username: 88 | user = i.username 89 | 90 | checkin = i.checkin 91 | if ndx < len(tiles_history) - 1 and tiles_history[ndx + 1].version == 1 or \ 92 | ndx == len(tiles_history) - 1: 93 | tile = session.query(Tile) \ 94 | .get((i.x, i.y, 9)) 95 | if user is not None and tile is not None: 96 | status = compare_checkin(checkin, tile.checkin) 97 | print "status %s" % status 98 | checkin = 0 99 | user = None 100 | """ 101 | -------------------------------------------------------------------------------- /migration/versions/data_upgrade_010.py: -------------------------------------------------------------------------------- 1 | """ this script is made to upgrade data from 9 -> 10 """ 2 | from sqlalchemy.engine import create_engine 3 | engine = create_engine('sqlite:///OSMTM.db') 4 | connection = engine.connect() 5 | 6 | from OSMTM.models import TileHistory, Tile, Job 7 | from sqlalchemy import orm 8 | from sqlalchemy.sql.expression import and_ 9 | 10 | sm = orm.sessionmaker(bind=engine, autoflush=True, autocommit=False, 11 | expire_on_commit=True) 12 | session = orm.scoped_session(sm) 13 | 14 | jobs = session.query(Job).all() 15 | 16 | for job in jobs: 17 | 18 | print "job: %s" % job.id 19 | tiles_h = session.query(TileHistory) \ 20 | .filter(TileHistory.job_id==job.id) 21 | for tile in tiles_h: 22 | tile.zoom = job.zoom 23 | session.add(tile) 24 | 25 | tiles = session.query(Tile) \ 26 | .filter(Tile.job_id==job.id) 27 | for tile in tiles: 28 | tile.zoom = job.zoom 29 | session.add(tile) 30 | 31 | session.commit() 32 | -------------------------------------------------------------------------------- /production.ini: -------------------------------------------------------------------------------- 1 | [app:OSMTM] 2 | use = egg:OSMTM 3 | reload_templates = false 4 | debug_authorization = false 5 | debug_notfound = false 6 | debug_routematch = false 7 | debug_templates = false 8 | default_locale_name = en 9 | sqlalchemy.url = sqlite:///%(here)s/OSMTM.db 10 | admin_user = pgiraud 11 | 12 | [filter:weberror] 13 | use = egg:WebError#error_catcher 14 | debug = false 15 | ;error_log = 16 | ;show_exceptions_in_wsgi_errors = true 17 | ;smtp_server = localhost 18 | ;error_email = janitor@example.com 19 | ;smtp_username = janitor 20 | ;smtp_password = "janitor's password" 21 | ;from_address = paste@localhost 22 | ;error_subject_prefix = "Pyramid Error" 23 | ;smtp_use_tls = 24 | ;error_message = 25 | 26 | [filter:tm] 27 | use = egg:repoze.tm2#tm 28 | commit_veto = repoze.tm:default_commit_veto 29 | 30 | [pipeline:main] 31 | pipeline = 32 | weberror 33 | tm 34 | OSMTM 35 | 36 | [server:main] 37 | use = egg:Paste#http 38 | host = 0.0.0.0 39 | port = 6543 40 | 41 | # Begin logging configuration 42 | 43 | [loggers] 44 | keys = root, OSMTM, sqlalchemy 45 | 46 | [handlers] 47 | keys = console 48 | 49 | [formatters] 50 | keys = generic 51 | 52 | [logger_root] 53 | level = WARN 54 | handlers = console 55 | 56 | [logger_OSMTM] 57 | level = WARN 58 | handlers = 59 | qualname = OSMTM 60 | 61 | [logger_sqlalchemy] 62 | level = WARN 63 | handlers = 64 | qualname = sqlalchemy.engine 65 | # "level = INFO" logs SQL queries. 66 | # "level = DEBUG" logs SQL queries and results. 67 | # "level = WARN" logs neither. (Recommended for production systems.) 68 | 69 | [handler_console] 70 | class = StreamHandler 71 | args = (sys.stderr,) 72 | level = NOTSET 73 | formatter = generic 74 | 75 | [formatter_generic] 76 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 77 | 78 | # End logging configuration 79 | -------------------------------------------------------------------------------- /scripts/delete.py: -------------------------------------------------------------------------------- 1 | #!env/bin/python 2 | __requires__ = 'OSMTM' 3 | 4 | import os 5 | import sys 6 | import transaction 7 | 8 | from sqlalchemy import create_engine 9 | 10 | from pyramid.paster import ( 11 | get_appsettings, 12 | setup_logging, 13 | ) 14 | 15 | from OSMTM.models import ( 16 | DBSession, 17 | Job, 18 | Tile, 19 | TileHistory, 20 | Base, 21 | ) 22 | 23 | from OSMTM.utils import * 24 | 25 | id = sys.argv[1] 26 | 27 | engine = create_engine('sqlite:///OSMTM.db') 28 | DBSession.configure(bind=engine) 29 | with transaction.manager: 30 | job = DBSession.query(Job).filter(Job.id==id).one() 31 | DBSession.delete(job) 32 | -------------------------------------------------------------------------------- /scripts/update.py: -------------------------------------------------------------------------------- 1 | #!env/bin/python 2 | __requires__ = 'OSMTM' 3 | 4 | import os 5 | import sys 6 | import transaction 7 | 8 | from sqlalchemy import create_engine 9 | 10 | from pyramid.paster import ( 11 | get_appsettings, 12 | setup_logging, 13 | ) 14 | 15 | from OSMTM.models import ( 16 | DBSession, 17 | Job, 18 | Tile, 19 | TileHistory, 20 | Base, 21 | ) 22 | 23 | from OSMTM.utils import * 24 | 25 | engine = create_engine('sqlite:///OSMTM.db') 26 | DBSession.configure(bind=engine) 27 | with transaction.manager: 28 | tile = DBSession.query(Tile).filter(Tile.job_id==292, Tile.x==19372, Tile.y==17805).one() 29 | tile.checkin = 1 30 | DBSession.add(tile) 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | match=^test 3 | nocapture=1 4 | cover-package=OSMTM 5 | with-coverage=1 6 | cover-erase=1 7 | 8 | [compile_catalog] 9 | directory = OSMTM/locale 10 | domain = OSMTM 11 | statistics = true 12 | 13 | [extract_messages] 14 | add_comments = TRANSLATORS: 15 | output_file = OSMTM/locale/OSMTM.pot 16 | width = 80 17 | 18 | [init_catalog] 19 | domain = OSMTM 20 | input_file = OSMTM/locale/OSMTM.pot 21 | output_dir = OSMTM/locale 22 | 23 | [update_catalog] 24 | domain = OSMTM 25 | input_file = OSMTM/locale/OSMTM.pot 26 | output_dir = OSMTM/locale 27 | previous = true 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | README = open(os.path.join(here, 'README.txt')).read() 8 | CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() 9 | 10 | requires = [ 11 | 'pyramid==1.4', 12 | 'pyramid_beaker', 13 | 'pyramid_debugtoolbar', 14 | 'SQLAlchemy==0.7.9', 15 | 'Shapely', 16 | 'transaction', 17 | 'repoze.tm2>=1.0b1', # default_commit_veto 18 | 'zope.sqlalchemy', 19 | 'WebError', 20 | 'oauth2==1.2.0', 21 | 'imposm.parser', 22 | 'geojson==1.0.1', 23 | 'WebTest==2.0.9', 24 | 'sqlalchemy-migrate', 25 | 'markdown', 26 | 'nose', 27 | 'coverage', 28 | 'beautifulSoup', 29 | 'pyshp==1.1.4', 30 | 'papyrus==0.7', 31 | 'simplejson', 32 | 'alembic' 33 | ] 34 | 35 | if sys.version_info[:3] < (2,5,0): 36 | requires.append('pysqlite') 37 | 38 | setup(name='OSMTM', 39 | version='0.2.1', 40 | description='OSMTM', 41 | long_description=README + '\n\n' + CHANGES, 42 | classifiers=[ 43 | "Programming Language :: Python", 44 | "Framework :: Pylons", 45 | "Topic :: Internet :: WWW/HTTP", 46 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 47 | ], 48 | author='', 49 | author_email='', 50 | url='', 51 | keywords='web wsgi bfg pylons pyramid', 52 | packages=find_packages(), 53 | include_package_data=True, 54 | zip_safe=False, 55 | test_suite='OSMTM', 56 | install_requires = requires, 57 | entry_points = """\ 58 | [paste.app_factory] 59 | main = OSMTM:main 60 | """, 61 | paster_plugins=['pyramid'], 62 | ) 63 | 64 | --------------------------------------------------------------------------------