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

RDISCROWD-5852 Calc task expiration by create date #836

Merged
merged 12 commits into from
Apr 18, 2023
6 changes: 4 additions & 2 deletions pybossa/api/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import json
import copy
from pybossa.task_creator_helper import get_task_expiration
from pybossa.model import make_timestamp


class TaskAPI(APIBase):
Expand Down Expand Up @@ -80,7 +81,7 @@ def _update_attribute(self, new, old):
new.id, old.state, new.state,
str(old.exported), str(new.exported))
if new.expiration is not None:
new.expiration = get_task_expiration(new.expiration)
new.expiration = get_task_expiration(new.expiration, old.created)

def _preprocess_post_data(self, data):
project_id = data["project_id"]
Expand Down Expand Up @@ -116,7 +117,8 @@ def _preprocess_post_data(self, data):
data['exported'] = True
except Exception as e:
raise BadRequest('Invalid gold_answers')
data["expiration"] = get_task_expiration(data.get('expiration'))
create_time = data.get("created") or make_timestamp()
data["expiration"] = get_task_expiration(data.get('expiration'), create_time)

def _verify_auth(self, item):
if not current_user.is_authenticated:
Expand Down
8 changes: 5 additions & 3 deletions pybossa/importers/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
from .s3 import BulkTaskS3Import
from .base import BulkImportException
from .usercsv import BulkUserCSVImport
from pybossa.util import (check_password_strength, valid_or_no_s3_bucket,
get_now_plus_delta_ts)
from pybossa.util import (check_password_strength, valid_or_no_s3_bucket)
from flask_login import current_user
from werkzeug.datastructures import MultiDict
import copy
Expand All @@ -41,6 +40,7 @@
from flask import url_for
from pybossa.task_creator_helper import set_gold_answers, upload_files_priv, get_task_expiration
from pybossa.data_access import data_access_levels
from pybossa.model import make_timestamp


def validate_s3_bucket(task, *args):
Expand Down Expand Up @@ -208,7 +208,9 @@ def create_tasks(self, task_repo, project, importer=None, **form_data):
try:
for task_data in tasks:
self.upload_private_data(task_data, project.id)
task_data['expiration'] = get_task_expiration(task_data.get('expiration'))
# As tasks are getting created, pass current date as create_date
create_date = make_timestamp()
task_data['expiration'] = get_task_expiration(task_data.get('expiration'), create_date)

task = Task(project_id=project.id, n_answers=n_answers)
[setattr(task, k, v) for k, v in task_data.items()]
Expand Down
4 changes: 4 additions & 0 deletions pybossa/repositories/task_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from flask import current_app
from sqlalchemy import or_
from sqlalchemy.sql import case as sqlalchemy_case
from pybossa.task_creator_helper import get_task_expiration


class TaskRepository(Repository):
Expand Down Expand Up @@ -179,6 +180,9 @@ def _filter_query(self, query, obj, limit, offset, last_id, yielded, desc):
def save(self, element, clean_project=True):
self._validate_can_be(self.SAVE_ACTION, element)
try:
# set task default expiration
if element.__class__.__name__ == "Task":
element.expiration = get_task_expiration(element.expiration, make_timestamp())
self.db.session.add(element)
self.db.session.commit()
if clean_project:
Expand Down
28 changes: 13 additions & 15 deletions pybossa/task_creator_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"""Module with PyBossa create task helper."""
from flask import current_app
import hashlib
import datetime
from pybossa.cloud_store_api.s3 import upload_json_data, get_content_from_s3
from pybossa.util import get_now_plus_delta_ts
from pybossa.util import get_time_plus_delta_ts
from flask import url_for
import json
from six import string_types
Expand All @@ -40,23 +41,21 @@ def s3_conn_type():
return current_app.config.get('S3_CONN_TYPE')


def get_task_expiration(current_expiration):
def get_task_expiration(expiration, create_time):
dchhabda marked this conversation as resolved.
Show resolved Hide resolved
"""
Find the appropriate expiration to be added to a task with expiring data.
If no expiration is set, return the data expiration; otherwise, return
the smallest between the current expiration and the data expiration.
current_expiration can be a iso datetime string or a datetime object
Given current task expiration, compute new expiration based on
1. task creation date and 2. max allowed task expiration
do that task expiration cannot be set beyond max task expiration
from task creation date
"""
validity = current_app.config.get('TASK_EXPIRATION', 60)
return _get_task_expiration(current_expiration, validity)
max_expiration_days = current_app.config.get('TASK_EXPIRATION', 60)
max_expiration = get_time_plus_delta_ts(create_time, days=max_expiration_days)

if expiration and isinstance(expiration, string_types):
max_expiration = max_expiration.isoformat()

def _get_task_expiration(current_expiration, validity):
task_exp = get_now_plus_delta_ts(days=validity)
if isinstance(current_expiration, string_types):
task_exp = task_exp.isoformat()
current_expiration = current_expiration or task_exp
return min(current_expiration, task_exp)
expiration = expiration or max_expiration
return min(expiration, max_expiration)


def set_gold_answers(task, gold_answers):
Expand All @@ -65,7 +64,6 @@ def set_gold_answers(task, gold_answers):
if encrypted():
url = upload_files_priv(task, task.project_id, gold_answers, TASK_PRIVATE_GOLD_ANSWER_FILE_NAME)['externalUrl']
gold_answers = dict([(TASK_GOLD_ANSWER_URL_KEY, url)])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think setting gold task should not be setting task expiration as both operations are independent and hence task expiration not to be modified with setting gold task.

@kbecker42 , @XiChenn , @n00rsy thoughts?

task.expiration = get_task_expiration(task.expiration)

task.gold_answers = gold_answers
task.calibration = 1
Expand Down
7 changes: 4 additions & 3 deletions pybossa/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1108,9 +1108,10 @@ def sign_task(task):
task['signature'] = signature


def get_now_plus_delta_ts(**kwargs):
return (datetime.utcnow() + timedelta(**kwargs))

def get_time_plus_delta_ts(time, **kwargs):
if isinstance(time, str):
time = datetime.fromisoformat(time)
return (time + timedelta(**kwargs))

def get_taskrun_date_range_sql_clause_params(start_date, end_date):
"""Generate date cause and sql params for queriying db."""
Expand Down
21 changes: 16 additions & 5 deletions test/test_api/test_task_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,13 @@ def test_task_put_with_reserved_fields_returns_error(self):
def test_task_put_with_expiration_within_bound(self):
admin = UserFactory.create()
project = ProjectFactory.create(owner=admin)
task = TaskFactory.create(project=project, info=dict(x=1))
expiration = (datetime.datetime.utcnow() + datetime.timedelta(30)).isoformat()
task = TaskFactory.create(
project=project,
info=dict(x=1),
created='2015-01-01T14:37:30.642119'
)
# 40 days after creation date
expiration = '2015-02-10T14:37:30.642119'
datajson = json.dumps({'expiration': expiration})

url = '/api/task/%s?api_key=%s' % (task.id, admin.api_key)
Expand All @@ -788,9 +793,15 @@ def test_task_put_with_expiration_within_bound(self):
def test_task_put_with_expiration_out_of_bounds(self):
admin = UserFactory.create()
project = ProjectFactory.create(owner=admin)
task = TaskFactory.create(project=project, info=dict(x=1))
expiration = (datetime.datetime.utcnow() + datetime.timedelta(1000)).isoformat()
max_expiration = (datetime.datetime.utcnow() + datetime.timedelta(60)).isoformat()
task = TaskFactory.create(
project=project,
info=dict(x=1),
created='2015-01-01T14:37:30.642119'
)
# the task expires 60 days after creation date
max_expiration = '2015-03-02T14:37:30.642119'
# 365 days after creation date
expiration = '2016-01-01T14:37:30.642119'
datajson = json.dumps({'expiration': expiration})

url = '/api/task/%s?api_key=%s' % (task.id, admin.api_key)
Expand Down
32 changes: 26 additions & 6 deletions test/test_task_creator_helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timedelta

from pybossa.task_creator_helper import _get_task_expiration
from pybossa.task_creator_helper import get_task_expiration
from test import with_context


def are_almost_equal(date1, date2):
Expand All @@ -15,35 +16,54 @@ def to_datetime(timestamp):

class TestGetTaskExpirationDatetime(object):

@with_context
def test_current_expiration_is_before(self):
now = datetime.utcnow()
current_exp = now + timedelta(days=30)
exp = _get_task_expiration(current_exp, 60)
exp = get_task_expiration(current_exp, now)
assert exp == current_exp

@with_context
def test_current_expiration_is_after(self):
now = datetime.utcnow()
current_exp = now + timedelta(days=90)
exp = _get_task_expiration(current_exp, 60)
exp = get_task_expiration(current_exp, now)
assert are_almost_equal(exp, now + timedelta(days=60))

@with_context
def test_create_date_is_set(self):
now = datetime.utcnow()
current_exp = now + timedelta(days=90)
exp = get_task_expiration(current_exp, now)
assert are_almost_equal(exp, now + timedelta(days=60))

@with_context
def test_current_expiration_is_none(self):
now = datetime.utcnow()
exp = _get_task_expiration(None, 60)
exp = get_task_expiration(None, now)
assert are_almost_equal(exp, now + timedelta(days=60))


class TestGetTaskExpirationString(object):

@with_context
def test_current_expiration_is_before(self):
now = datetime.utcnow()
current_exp = now + timedelta(days=30)
exp = _get_task_expiration(current_exp.isoformat(), 60)
exp = get_task_expiration(current_exp.isoformat(), now)
assert to_datetime(exp) == current_exp

@with_context
def test_current_expiration_is_after(self):
now = datetime.utcnow()
current_exp = now + timedelta(days=90)
exp = _get_task_expiration(current_exp.isoformat(), 60)
exp = get_task_expiration(current_exp.isoformat(), now)
assert are_almost_equal(
to_datetime(exp), now + timedelta(days=60))

@with_context
def test_create_date_is_set(self):
now = datetime.utcnow()
current_exp = now + timedelta(days=30)
exp = get_task_expiration(current_exp.isoformat(), now)
assert to_datetime(exp) == current_exp