Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make task auto-unlock duration configurable #1274

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/app/project/project.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@
}
if (task != null && task.taskId in vm.lockTime) {
var lockTime = moment.utc(vm.lockTime[task.taskId]);
return lockTime.add(2, 'hours').diff(moment.utc(), 'minutes');
return lockTime.add(task.autoUnlockSeconds, 'seconds').diff(moment.utc(), 'minutes');
}
else {
return null;
Expand Down
2 changes: 1 addition & 1 deletion server/api/project_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def get(self, project_id):
current_app.logger.critical(error_msg)
return {"error": error_msg}, 500
finally:
# this will try to unlock tasks older than 2 hours
# this will try to unlock tasks that have been locked too long
try:
ProjectService.auto_unlock_tasks(project_id)
except Exception as e:
Expand Down
4 changes: 4 additions & 0 deletions server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class EnvironmentConfig:
# Mapper Level values represent number of OSM changesets
MAPPER_LEVEL_INTERMEDIATE = 250
MAPPER_LEVEL_ADVANCED = 500
# Time to wait until task auto-unlock,
# e.g. '2h' (2 hours) or '7d' (7 days) or '30m' (30 minutes) or '1h30m' (1.5 hours)
TASK_AUTOUNLOCK_AFTER = '2h'

OSM_OAUTH_SETTINGS = {
'base_url': 'https://www.openstreetmap.org/api/0.6/',
'consumer_key': os.getenv('TM_CONSUMER_KEY', None),
Expand Down
1 change: 1 addition & 0 deletions server/models/dtos/mapping_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class TaskDTO(Model):
task_history = ListType(ModelType(TaskHistoryDTO), serialized_name='taskHistory')
per_task_instructions = StringType(serialized_name='perTaskInstructions', serialize_when_none=False)
is_undoable = BooleanType(serialized_name='isUndoable', default=False)
auto_unlock_seconds = IntType(serialized_name='autoUnlockSeconds')


class TaskDTOs(Model):
Expand Down
14 changes: 10 additions & 4 deletions server/models/postgis/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import geojson
import json
from enum import Enum
from flask import current_app
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from sqlalchemy.orm.session import make_transient
from geoalchemy2 import Geometry
Expand All @@ -13,7 +14,7 @@
from server.models.dtos.project_dto import ProjectComment, ProjectCommentsDTO
from server.models.postgis.statuses import TaskStatus, MappingLevel
from server.models.postgis.user import User
from server.models.postgis.utils import InvalidData, InvalidGeoJson, ST_GeomFromGeoJSON, ST_SetSRID, timestamp, NotFound
from server.models.postgis.utils import InvalidData, InvalidGeoJson, ST_GeomFromGeoJSON, ST_SetSRID, timestamp, parse_duration, NotFound


class TaskAction(Enum):
Expand Down Expand Up @@ -319,10 +320,14 @@ def get_all_tasks(project_id: int):
""" Get all tasks for a given project """
return Task.query.filter(Task.project_id == project_id).all()

@staticmethod
def auto_unlock_delta():
return parse_duration(current_app.config['TASK_AUTOUNLOCK_AFTER'])

@staticmethod
def auto_unlock_tasks(project_id: int):
"""Unlock all tasks locked more than 2 hours ago"""
expiry_delta = datetime.timedelta(hours=2)
"""Unlock all tasks locked for longer than the auto-unlock delta"""
expiry_delta = Task.auto_unlock_delta()
lock_duration = (datetime.datetime.min + expiry_delta).time().isoformat()
expiry_date = datetime.datetime.utcnow() - expiry_delta
old_locks_query = '''SELECT t.id
Expand All @@ -339,7 +344,7 @@ def auto_unlock_tasks(project_id: int):
old_tasks = db.engine.execute(old_locks_query)

if old_tasks.rowcount == 0:
# no tasks older than 2 hours found, return without further processing
# no tasks older than the delta found, return without further processing
return

for old_task in old_tasks:
Expand Down Expand Up @@ -558,6 +563,7 @@ def as_dto_with_instructions(self, preferred_locale: str = 'en') -> TaskDTO:
task_dto.task_status = TaskStatus(self.task_status).name
task_dto.lock_holder = self.lock_holder.username if self.lock_holder else None
task_dto.task_history = task_history
task_dto.auto_unlock_seconds = Task.auto_unlock_delta().total_seconds()

per_task_instructions = self.get_per_task_instructions(preferred_locale)

Expand Down
16 changes: 16 additions & 0 deletions server/models/postgis/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import json
import re
from flask import current_app
from geoalchemy2 import Geometry
from geoalchemy2.functions import GenericFunction
Expand Down Expand Up @@ -88,6 +89,21 @@ def timestamp():
return datetime.datetime.utcnow()


# Based on https://stackoverflow.com/a/51916936
duration_regex = re.compile(r'^((?P<days>[\.\d]+?)d)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$')
def parse_duration(time_str):
"""
Parse a duration string e.g. (2h13m) into a timedelta object.

:param time_str: A string identifying a duration. (eg. 2h13m)
:return datetime.timedelta: A datetime.timedelta object
"""
parts = duration_regex.match(time_str)
assert parts is not None, "Could not parse duration from '{}'".format(time_str)
time_params = {name: float(param) for name, param in parts.groupdict().items() if param}
return datetime.timedelta(**time_params)


class DateTimeEncoder(json.JSONEncoder):
"""
Custom JSON Encoder that handles Python date/times
Expand Down