class TaskGeometry(db.Model): """The collection of geometries (1+) belonging to a task""" __tablename__ = 'task_geometries' id = db.Column(db.Integer, nullable=False, unique=True, primary_key=True) osmid = db.Column(db.BigInteger) task_id = db.Column(db.Integer, db.ForeignKey('tasks.id', onupdate="cascade", ondelete="cascade"), nullable=False) geom = db.Column(Geometry, nullable=False) def __init__(self, shape, osmid=None): self.osmid = osmid self.geom = from_shape(shape) @hybrid_property def geometry(self): """Return the task geometry collection as a Shapely object""" return to_shape(self.geom) @geometry.setter def geometry(self, shape): """Set the task geometry collection from a Shapely object""" self.geom = from_shape(shape) geometry = synonym('geom', descriptor=geometry)
class Action(db.Model): """An action on a task""" __tablename__ = 'actions' id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False, autoincrement=True) timestamp = db.Column( db.DateTime, # store the timestamp as naive UTC time default=datetime.now(pytz.utc).replace(tzinfo=None), nullable=False) user_id = db.Column( db.Integer, db.ForeignKey('users.id', onupdate="cascade", ondelete="cascade")) task_id = db.Column( db.Integer, db.ForeignKey('tasks.id', onupdate="cascade", ondelete="cascade")) status = db.Column(db.String(), nullable=False) editor = db.Column(db.String()) __table_args__ = (db.Index('idx_action_timestamp', timestamp), db.Index('idx_action_userid', user_id), db.Index('idx_action_taskid', task_id), db.Index('idx_action_status', status)) def __repr__(self): return "<Action %s set on %s>" % (self.status, self.timestamp) def __init__(self, status, user_id=None, editor=None): self.status = status # store the timestamp as naive UTC time self.timestamp = datetime.now(pytz.utc).replace(tzinfo=None) if user_id: self.user_id = user_id if editor: self.editor = editor
class Task(db.Model): """A MapRoulette task""" __tablename__ = 'tasks' id = db.Column(db.Integer, Sequence('tasks_id_seq'), unique=True, nullable=False) identifier = db.Column(db.String(72), primary_key=True, nullable=False) challenge_slug = db.Column(db.String, db.ForeignKey('challenges.slug', onupdate="cascade", ondelete="cascade"), primary_key=True) random = db.Column(db.Float, default=getrandom, nullable=False) manifest = db.Column(db.String) # not used for now location = db.Column(Geometry) geometries = db.relationship("TaskGeometry", cascade='all,delete-orphan', passive_deletes=True, backref=db.backref("task")) actions = db.relationship("Action", cascade='all,delete-orphan', passive_deletes=True, backref=db.backref("task")) status = db.Column(db.String) instruction = db.Column(db.String) # note that spatial indexes seem to be created automagically __table_args__ = (db.Index('idx_id', id), db.Index('idx_identifer', identifier), db.Index('idx_challenge', challenge_slug), db.Index('idx_random', random)) # geometries should always be provided for new tasks, defaulting to None so # we can handle task updates and two step initialization of tasks def __init__(self, challenge_slug, identifier, geometries=[], instruction=None, status='created'): self.challenge_slug = challenge_slug self.identifier = identifier self.instruction = instruction self.geometries = geometries self.append_action(Action('created')) def __repr__(self): return '<Task {identifier}>'.format(identifier=self.identifier) def __str__(self): return self.identifier @hybrid_method def has_status(self, statuses): if not type(statuses) == list: statuses = [statuses] return self.status in statuses @has_status.expression def has_status(cls, statuses): if not type(statuses) == list: statuses = [statuses] return cls.status.in_(statuses) def update(self, new_values, geometries, commit=True): """This updates a task based on a dict with new values""" for k, v in new_values.iteritems(): # if a status is set, append an action if k == 'status': self.append_action(Action(v)) elif not hasattr(self, k): app.logger.debug('task does not have %s' % (k, )) return False setattr(self, k, v) self.geometries = geometries # set the location for this task, as a representative point of the # combined geometries. if self.location is None: self.set_location() db.session.add(self) if commit: try: db.session.commit() except Exception as e: app.logger.warn(e.message) db.session.rollback() raise e return True @property def is_within(self, lon, lat, radius): return self.location.intersects(Point(lon, lat).buffer(radius)) def append_action(self, action): self.actions.append(action) # duplicate the action status string in the tasks table to save lookups self.status = action.status try: db.session.commit() except Exception as e: app.logger.warn(e.message) db.session.rollback() raise e def set_location(self): """Set the location of a task as a cheaply calculated representative point of the combined geometries.""" # set the location field, which is a representative point # for the task's geometries # first, get all individual coordinates for the geometries coordinates = [] for geometry in self.geometries: coordinates.extend(list(to_shape(geometry.geom).coords)) # then, set the location to a representative point # (cheaper than centroid) if len(coordinates) > 0: self.location = from_shape( MultiPoint(coordinates).representative_point(), srid=4326)