diff --git a/crc/api/study.py b/crc/api/study.py index bbdcad01..593895a1 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -102,7 +102,7 @@ def user_studies(): if len(studies) == 0: studies = StudyService().get_studies_for_user(user, include_invalid=True) if len(studies) > 0: - message = f"All studies associated with User: {user.display_name} failed study validation" + message = f"All studies associated with User: {user.uid} failed study validation" raise ApiError(code="study_integrity_error", message=message) results = StudySchema(many=True).dump(studies) diff --git a/crc/models/email.py b/crc/models/email.py index cf2bb543..96d3227b 100644 --- a/crc/models/email.py +++ b/crc/models/email.py @@ -26,5 +26,5 @@ class EmailModelSchema(ma.Schema): class Meta: model = EmailModel - fields = ["id", "subject", "sender", "recipients", "cc", "bcc", "content", "content_html", + fields = ["id", "subject", "sender", "recipients", "cc", "bcc", "content", "study_id", "timestamp", "workflow_spec_id"] diff --git a/crc/models/task_log.py b/crc/models/task_log.py new file mode 100644 index 00000000..0636c113 --- /dev/null +++ b/crc/models/task_log.py @@ -0,0 +1,23 @@ +from crc import db, ma +from crc.models.study import StudyModel +from crc.models.workflow import WorkflowModel +from sqlalchemy import func + + +class TaskLogModel(db.Model): + __tablename__ = 'task_log' + id = db.Column(db.Integer, primary_key=True) + level = db.Column(db.String) + code = db.Column(db.String) + message = db.Column(db.String) + study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False) + workflow_id = db.Column(db.Integer, db.ForeignKey(WorkflowModel.id), nullable=False) + task = db.Column(db.String) + timestamp = db.Column(db.DateTime(timezone=True), default=func.now()) + + +class TaskLogModelSchema(ma.Schema): + + class Meta: + model = TaskLogModel + fields = ["id", "level", "code", "message", "study_id", "workflow_id", "timestamp"] diff --git a/crc/scripts/email.py b/crc/scripts/email.py index b732d2ea..40f1c744 100644 --- a/crc/scripts/email.py +++ b/crc/scripts/email.py @@ -1,5 +1,6 @@ import sys import traceback +import datetime from crc import app, session from crc.api.common import ApiError @@ -45,7 +46,8 @@ email(subject="My Subject", recipients="user@example.com", attachments=['Study_A subject = self.get_subject(kwargs['subject']) recipients = self.get_email_addresses(kwargs['recipients'], study_id) content, content_html = EmailService().get_rendered_content(task.task_spec.documentation, task.data) - email_model = EmailModel(subject=subject, + email_model = EmailModel(id=1, + subject=subject, recipients=recipients, content=content, content_html=content_html, diff --git a/crc/scripts/get_email_data.py b/crc/scripts/get_email_data.py index c3539823..4f787ddb 100644 --- a/crc/scripts/get_email_data.py +++ b/crc/scripts/get_email_data.py @@ -2,7 +2,9 @@ from crc.scripts.script import Script from crc.api.common import ApiError from crc import session from crc.models.email import EmailModel, EmailModelSchema -import json +from crc.services.email_service import EmailService + +import datetime class EmailData(Script): @@ -12,11 +14,23 @@ class EmailData(Script): def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): if 'email_id' in kwargs or 'workflow_spec_id' in kwargs: - return True - else: - return False + subject = 'My Test Email' + recipients = 'user@example.com' + content = "Hello" + content_html = "

Hello

" + email_model = EmailModel(subject=subject, + recipients=recipients, + content=content, + content_html=content_html, + timestamp=datetime.datetime.utcnow()) + return EmailModelSchema(many=True).dump([email_model]) - def do_task(self, task, study_id, workflow_id, **kwargs): + else: + raise ApiError.from_task(code='missing_email_id', + message='You must include an email_id or workflow_spec_id with the get_email_data script.', + task=task) + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): email_models = None email_data = None if 'email_id' in kwargs: diff --git a/crc/scripts/get_localtime.py b/crc/scripts/get_localtime.py index c3add55e..edc912e0 100644 --- a/crc/scripts/get_localtime.py +++ b/crc/scripts/get_localtime.py @@ -1,5 +1,3 @@ -import datetime - from crc.api.common import ApiError from crc.scripts.script import Script @@ -14,16 +12,21 @@ class GetLocaltime(Script): Defaults to US/Eastern""" def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): - if 'timestamp' in kwargs: - return datetime.datetime.now() + if len(args) > 0 or 'timestamp' in kwargs: + return self.do_task(task, study_id, workflow_id, *args, **kwargs) raise ApiError(code='missing_timestamp', message='You must include a timestamp to convert.') def do_task(self, task, study_id, workflow_id, *args, **kwargs): - if 'timestamp' in kwargs: - timestamp = kwargs['timestamp'] + if len(args) > 0 or 'timestamp' in kwargs: + if 'timestamp' in kwargs: + timestamp = kwargs['timestamp'] + else: + timestamp = args[0] if 'timezone' in kwargs: timezone = kwargs['timezone'] + elif len(args) > 1: + timezone = args[1] else: timezone = 'US/Eastern' parsed_timestamp = dateparser.parse(timestamp) diff --git a/crc/scripts/get_logs.py b/crc/scripts/get_logs.py new file mode 100644 index 00000000..53e3d667 --- /dev/null +++ b/crc/scripts/get_logs.py @@ -0,0 +1,38 @@ +from crc import session +from crc.models.task_log import TaskLogModel, TaskLogModelSchema +from crc.scripts.script import Script + + +class GetLogsByWorkflow(Script): + + def get_description(self): + return """Script to retrieve logs for the current workflow. + Accepts an optional `code` argument that is used to filter the DB query. + """ + + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): + log_model = TaskLogModel(level='info', + code='mocked_code', + message='This is my logging message', + study_id=study_id, + workflow_id=workflow_id, + task=task.get_name()) + TaskLogModelSchema(many=True).dump([log_model]) + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): + code = None + if 'code' in kwargs: + code = kwargs['code'] + elif len(args) > 0: + code = args[0] + if code is not None: + log_models = session.query(TaskLogModel).\ + filter(TaskLogModel.code == code).\ + filter(TaskLogModel.workflow_id == workflow_id).\ + all() + else: + log_models = session.query(TaskLogModel). \ + filter(TaskLogModel.workflow_id == workflow_id). \ + all() + + return TaskLogModelSchema(many=True).dump(log_models) diff --git a/crc/scripts/get_logs_for_study.py b/crc/scripts/get_logs_for_study.py new file mode 100644 index 00000000..f02a25ed --- /dev/null +++ b/crc/scripts/get_logs_for_study.py @@ -0,0 +1,38 @@ +from crc import session +from crc.models.task_log import TaskLogModel, TaskLogModelSchema +from crc.scripts.script import Script + + +class GetLogsByWorkflow(Script): + + def get_description(self): + return """Script to retrieve logs for the current study. + Accepts an optional `code` argument that is used to filter the DB query. + """ + + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): + log_model = TaskLogModel(level='info', + code='mocked_code', + message='This is my logging message', + study_id=study_id, + workflow_id=workflow_id, + task=task.get_name()) + return TaskLogModelSchema(many=True).dump([log_model]) + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): + code = None + if 'code' in kwargs: + code = kwargs['code'] + elif len(args) > 0: + code = args[0] + if code is not None: + log_models = session.query(TaskLogModel).\ + filter(TaskLogModel.code == code).\ + filter(TaskLogModel.study_id == study_id).\ + all() + else: + log_models = session.query(TaskLogModel). \ + filter(TaskLogModel.study_id == study_id). \ + all() + + return TaskLogModelSchema(many=True).dump(log_models) diff --git a/crc/scripts/log.py b/crc/scripts/log.py new file mode 100644 index 00000000..3f33b603 --- /dev/null +++ b/crc/scripts/log.py @@ -0,0 +1,64 @@ +from crc import session +from crc.api.common import ApiError +from crc.models.task_log import TaskLogModel, TaskLogModelSchema +from crc.scripts.script import Script + + +class TaskLog(Script): + + def get_description(self): + return """Script to log events in a Script Task. + Takes `level`, `code`, and `message` arguments. + Example: + log(level='info', code='missing_info', message='You must include the correct info!') + + Level must be `debug`, `info`, `warning`, `error` or `critical`. + Code is a short string meant for searching the logs. By convention, it is lower case with underscores. + Message is a more descriptive string, including any info you want to log. + """ + + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): + if len(args) == 3 or ('level' in kwargs and 'code' in kwargs and 'message' in kwargs): + log_model = TaskLogModel(level='info', + code='mocked_code', + message='This is my logging message', + study_id=study_id, + workflow_id=workflow_id, + task=task.get_name()) + return TaskLogModelSchema().dump(log_model) + else: + raise ApiError.from_task(code='missing_arguments', + message='You must include a level, code, and message to log.', + task=task) + + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): + if len(args) == 3 or ('level' in kwargs and 'code' in kwargs and 'message' in kwargs): + if 'level' in kwargs: + level = kwargs['level'] + else: + level = args[0] + if 'code' in kwargs: + code = kwargs['code'] + else: + code = args[1] + if 'message' in kwargs: + message = kwargs['message'] + else: + message = args[2] + task_name = task.get_name() + log_model = TaskLogModel(level=level, + code=code, + message=message, + study_id=study_id, + workflow_id=workflow_id, + task=task_name) + session.add(log_model) + session.commit() + + return TaskLogModelSchema().dump(log_model) + + else: + raise ApiError.from_task(code='missing_arguments', + message='You must include a level, code, and message to log.', + task=task) diff --git a/crc/scripts/script.py b/crc/scripts/script.py index edb3cbf5..556b2a74 100644 --- a/crc/scripts/script.py +++ b/crc/scripts/script.py @@ -13,12 +13,12 @@ class Script(object): raise ApiError("invalid_script", "This script does not supply a description.") - def do_task(self, task, study_id, workflow_id, **kwargs): + def do_task(self, task, study_id, workflow_id, *args, **kwargs): raise ApiError("invalid_script", "This is an internal error. The script you are trying to execute '%s' " % self.__class__.__name__ + "does not properly implement the do_task function.") - def do_task_validate_only(self, task, study_id, workflow_id, **kwargs): + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): raise ApiError("invalid_script", "This is an internal error. The script you are trying to execute '%s' " % self.__class__.__name__ + "does must provide a validate_only option that mimics the do_task, " + diff --git a/crc/services/file_service.py b/crc/services/file_service.py index aea6ba61..6a148b68 100644 --- a/crc/services/file_service.py +++ b/crc/services/file_service.py @@ -45,13 +45,15 @@ class FileService(object): def add_workflow_spec_file(workflow_spec: WorkflowSpecModel, name, content_type, binary_data, primary=False, is_status=False): """Create a new file and associate it with a workflow spec.""" - # Raise ApiError if the file already exists - if session.query(FileModel)\ + file_model = session.query(FileModel)\ .filter(FileModel.workflow_spec_id == workflow_spec.id)\ - .filter(FileModel.name == name).first(): + .filter(FileModel.name == name).first() - raise ApiError(code="Duplicate File", - message='If you want to replace the file, use the update mechanism.') + if file_model: + if not file_model.archived: + # Raise ApiError if the file already exists and is not archived + raise ApiError(code="duplicate_file", + message='If you want to replace the file, use the update mechanism.') else: file_model = FileModel( workflow_spec_id=workflow_spec.id, @@ -60,7 +62,7 @@ class FileService(object): is_status=is_status, ) - return FileService.update_file(file_model, binary_data, content_type) + return FileService.update_file(file_model, binary_data, content_type) diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index 54150ea2..09409445 100755 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -363,6 +363,10 @@ class WorkflowService(object): def evaluate_property(property_name, field, task): expression = field.get_property(property_name) data = task.data + # If there's a field key with no initial value, give it one (None) + for field in task.task_spec.form.fields: + if field.id not in data: + data[field.id] = None if field.has_property(Task.FIELD_PROP_REPEAT): # Then you must evaluate the expression based on the data within the group, if that data exists. # There may not be data available in the group, if no groups where added diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 182048dd..95377c21 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -2,7 +2,7 @@ alabaster==0.7.12 alembic==1.4.3 aniso8601==8.0.0 attrs==20.3.0 -babel==2.9.0 +babel==2.9.1 bcrypt==3.2.0 beautifulsoup4==4.9.3 blinker==1.4 diff --git a/migrations/versions/a4f87f90cc64_add_log_table.py b/migrations/versions/a4f87f90cc64_add_log_table.py new file mode 100644 index 00000000..ec981b66 --- /dev/null +++ b/migrations/versions/a4f87f90cc64_add_log_table.py @@ -0,0 +1,36 @@ +"""add log table + +Revision ID: a4f87f90cc64 +Revises: ba6df7e560a1 +Create Date: 2021-10-27 10:54:40.233325 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a4f87f90cc64' +down_revision = 'ba6df7e560a1' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('task_log', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('level', sa.String(), nullable=True), + sa.Column('code', sa.String(), nullable=True), + sa.Column('message', sa.String(), nullable=True), + sa.Column('study_id', sa.Integer(), nullable=False), + sa.Column('workflow_id', sa.Integer(), nullable=False), + sa.Column('task', sa.String(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['study_id'], ['study.id'], ), + sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('task_log') diff --git a/tests/data/get_localtime/get_localtime.bpmn b/tests/data/get_localtime/get_localtime.bpmn index 43a136d5..e8d91c63 100644 --- a/tests/data/get_localtime/get_localtime.bpmn +++ b/tests/data/get_localtime/get_localtime.bpmn @@ -1,58 +1,85 @@ - + Flow_0lnc9x0 - - - + + + Flow_0kgtoh1 - - This is my email - Flow_0lnc9x0 - Flow_0gtgzcf - email_model = email(subject='My Email Subject', recipients='user@example.com') - - + timestamp = email_model.timestamp localtime = get_localtime(str(timestamp)) Flow_0gtgzcf Flow_0k1hbif - timestamp=email_model.timestamp -localtime = get_localtime(timestamp=timestamp) + if with_timestamp: + if with_timezone: + localtime_with = get_localtime(timestamp=timestamp, timezone=timezone) + localtime_without = get_localtime(timestamp, timezone) + else: + localtime_with = get_localtime(timestamp=timestamp) + localtime_without = get_localtime(timestamp) +else: + localtime = get_localtime() # Timestamp {{ timestamp }} +# Timezone +{{ timezone }} -# Localtime -{{ localtime }} +# Localtime With +{{ localtime_with }} + +# Localtime Without +{{ localtime_without }} Flow_0k1hbif Flow_0kgtoh1 + + This is my email + + + + + + + + + + + + + + + + + Flow_0lnc9x0 + Flow_0gtgzcf + - - - - - - - + + + - - - + + + + + + + @@ -60,15 +87,15 @@ localtime = get_localtime(timestamp=timestamp) - - - - + + + + diff --git a/tests/data/get_logging/get_logging.bpmn b/tests/data/get_logging/get_logging.bpmn new file mode 100644 index 00000000..d67463a2 --- /dev/null +++ b/tests/data/get_logging/get_logging.bpmn @@ -0,0 +1,96 @@ + + + + + Flow_0d5wpav + + + Flow_0pc42yp + Flow_0n34cdi + log_model_info = log(level='info', code='test_code', message='You forgot to include the correct data.') +log_model_debug = log(level='degug', code='debug_test_code', message='This is my debugging message') + + + # Logging Models Pre +{{ logging_models_pre }} + +# Log Model +{{ log_model }} + +# Logging Models All Post +{{ logging_models_all_post }} + + +# Logging Models Info Post +{{ logging_models_info_post }} + + +# Logging Models Debug Post +{{ logging_models_debug_post }} + Flow_07j4f0v + Flow_016ui0e + + + Flow_016ui0e + + + + Flow_0d5wpav + Flow_0pc42yp + logging_models_pre = get_logs() + + + Flow_0n34cdi + Flow_07j4f0v + logging_models_all_post = get_logs() +logging_models_info_post = get_logs('test_code') +logging_models_debug_post = get_logs('debug_test_code') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/get_logging_for_study/get_logging_for_study.bpmn b/tests/data/get_logging_for_study/get_logging_for_study.bpmn new file mode 100644 index 00000000..e3de15e6 --- /dev/null +++ b/tests/data/get_logging_for_study/get_logging_for_study.bpmn @@ -0,0 +1,90 @@ + + + + + Flow_0bbqksl + + + + # Hello +You may manipulate this in a test, as you see fit + Flow_0bbqksl + Flow_0lh4lq8 + + + + Flow_0lh4lq8 + Flow_10fc3fk + some_text = 'variable' +log('info', 'some_code', 'Some longer message') +log('info', 'some_other_code', 'Another really long message') +log('debug', 'debug_code', f'This message has a { some_text }!') + + + + Flow_10fc3fk + Flow_1dfqchi + workflow_logs = get_logs() +study_logs = get_logs_for_study() + + + + # Display Info + +## Workflow Logs +{{ workflow_logs }} + + +## Study Logs +{{ study_logs }} + Flow_1dfqchi + Flow_0yxmlin + + + Flow_0yxmlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/logging_task/logging_task.bpmn b/tests/data/logging_task/logging_task.bpmn new file mode 100644 index 00000000..22193e0b --- /dev/null +++ b/tests/data/logging_task/logging_task.bpmn @@ -0,0 +1,54 @@ + + + + + Flow_1vjxvjd + + + + Flow_1vjxvjd + Flow_1mw0dlv + log_model = log(level='info', code='test_code', message='You forgot to include the correct data.') + + + + # Log Model +{{ log_model }} + + Flow_1mw0dlv + Flow_016ui0e + + + Flow_016ui0e + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/files/test_files_api.py b/tests/files/test_files_api.py index 5ffd124b..43bd3d0c 100644 --- a/tests/files/test_files_api.py +++ b/tests/files/test_files_api.py @@ -5,7 +5,7 @@ import os from tests.base_test import BaseTest from crc import session, db, app -from crc.models.file import FileModel, FileType, FileSchema, FileModelSchema +from crc.models.file import FileModel, FileType, FileModelSchema, FileDataModel from crc.models.workflow import WorkflowSpecModel from crc.services.file_service import FileService from crc.services.workflow_processor import WorkflowProcessor @@ -13,6 +13,8 @@ from crc.models.data_store import DataStoreModel from crc.services.document_service import DocumentService from example_data import ExampleDataLoader +from sqlalchemy import desc + class TestFilesApi(BaseTest): @@ -376,3 +378,48 @@ class TestFilesApi(BaseTest): json_data = json.loads(rv.get_data(as_text=True)) self.assertTrue(json_data['primary']) self.assertIsNotNone(json_data['primary_process_id']) + + def test_file_upload_with_previous_name(self): + self.load_example_data() + workflow_spec_model = session.query(WorkflowSpecModel).first() + + # Add file + data = {'file': (io.BytesIO(b'asdf'), 'test_file.xlsx')} + rv = self.app.post('/v1.0/file?workflow_spec_id=%s' % workflow_spec_model.id, + data=data, + follow_redirects=True, + content_type='multipart/form-data', + headers=self.logged_in_headers()) + + self.assert_success(rv) + file_json = json.loads(rv.get_data(as_text=True)) + file_id = file_json['id'] + + # Set file to archived + file_model = session.query(FileModel).filter_by(id=file_id).first() + file_model.archived = True + session.commit() + + # Assert we have the correct file data and the file is archived + file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model_id == file_model.id).first() + self.assertEqual(b'asdf', file_data_model.data) + file_model = session.query(FileModel).filter_by(id=file_model.id).first() + self.assertEqual(True, file_model.archived) + + # Upload file with same name + data = {'file': (io.BytesIO(b'xyzpdq'), 'test_file.xlsx')} + rv = self.app.post('/v1.0/file?workflow_spec_id=%s' % workflow_spec_model.id, + data=data, + follow_redirects=True, + content_type='multipart/form-data', + headers=self.logged_in_headers()) + + self.assert_success(rv) + file_json = json.loads(rv.get_data(as_text=True)) + file_id = file_json['id'] + + # Assert we have the correct file data and the file is *not* archived + file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model_id == file_id).order_by(desc(FileDataModel.version)).first() + self.assertEqual(b'xyzpdq', file_data_model.data) + file_model = session.query(FileModel).filter_by(id=file_id).first() + self.assertEqual(False, file_model.archived) diff --git a/tests/scripts/test_get_email_data.py b/tests/scripts/test_get_email_data.py index b3b648d6..b9e45a37 100644 --- a/tests/scripts/test_get_email_data.py +++ b/tests/scripts/test_get_email_data.py @@ -6,6 +6,12 @@ from crc.services.email_service import EmailService class TestGetEmailData(BaseTest): + def test_email_data_validation(self): + self.load_example_data() + spec_model = self.load_test_spec('get_email_data') + rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers()) + self.assertEqual([], rv.json) + def test_get_email_data_by_email_id(self): self.load_example_data() workflow = self.create_workflow('get_email_data') @@ -32,6 +38,8 @@ class TestGetEmailData(BaseTest): self.assertEqual('My Email Subject', email_data[0]['subject']) self.assertEqual('sender@example.com', email_data[0]['sender']) self.assertEqual('[\'joe@example.com\']', email_data[0]['recipients']) + # Make sure we remove content_html from email_data + self.assertNotIn('content_html', email_data[0]) def test_get_email_data_by_workflow_spec_id(self): self.load_example_data() diff --git a/tests/scripts/test_get_localtime.py b/tests/scripts/test_get_localtime.py index 4abac7ea..de54a982 100644 --- a/tests/scripts/test_get_localtime.py +++ b/tests/scripts/test_get_localtime.py @@ -1,6 +1,7 @@ from tests.base_test import BaseTest from crc.scripts.get_localtime import GetLocaltime import dateparser +import datetime class TestGetLocaltime(BaseTest): @@ -8,11 +9,51 @@ class TestGetLocaltime(BaseTest): def test_get_localtime(self): self.load_example_data() + timestamp = datetime.datetime.utcnow() workflow = self.create_workflow('get_localtime') + workflow_api = self.get_workflow_api(workflow) task = workflow_api.next_task - timestamp = task.data['timestamp'] - localtime = task.data['localtime'] + workflow_api = self.complete_form(workflow, task, {'with_timestamp': True, + 'with_timezone': False, + 'timestamp': str(timestamp)}) + task = workflow_api.next_task - self.assertEqual(dateparser.parse(localtime), GetLocaltime().do_task(None, None, None, timestamp=timestamp)) + # The workflow calls get_localtime twice, once with named arguments and once without + localtime_with = task.data['localtime_with'] + localtime_without = task.data['localtime_without'] + + self.assertEqual(dateparser.parse(localtime_with), GetLocaltime().do_task(None, None, None, timestamp=str(timestamp))) + self.assertEqual(dateparser.parse(localtime_without), GetLocaltime().do_task(None, None, None, str(timestamp))) + + def test_get_localtime_with_timezone(self): + self.load_example_data() + + timestamp = datetime.datetime.utcnow() + workflow = self.create_workflow('get_localtime') + + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + workflow_api = self.complete_form(workflow, task, {'with_timestamp': True, + 'with_timezone': True, + 'timestamp': str(timestamp), + 'timezone': 'US/Eastern'}) + task = workflow_api.next_task + + # The workflow calls get_localtime twice, once with named arguments and once without + localtime_with = task.data['localtime_with'] + localtime_without = task.data['localtime_without'] + + self.assertEqual(dateparser.parse(localtime_with), GetLocaltime().do_task(None, None, None, timestamp=str(timestamp), timezone='US/Eastern')) + self.assertEqual(dateparser.parse(localtime_without), GetLocaltime().do_task(None, None, None, str(timestamp), 'US/Eastern')) + + def test_get_localtime_no_timestamp(self): + workflow = self.create_workflow('get_localtime') + + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + with self.assertRaises(AssertionError): + self.complete_form(workflow, task, {'with_timestamp': False, 'with_timezone': False}) diff --git a/tests/scripts/test_task_logging.py b/tests/scripts/test_task_logging.py new file mode 100644 index 00000000..1e348165 --- /dev/null +++ b/tests/scripts/test_task_logging.py @@ -0,0 +1,99 @@ +from tests.base_test import BaseTest + +from crc import session +from crc.models.api_models import Task +from crc.models.task_log import TaskLogModel +from crc.models.study import StudyModel +from crc.scripts.log import TaskLog + +import types + + +class TestTaskLogging(BaseTest): + + def test_logging_validation(self): + self.load_example_data() + spec_model = self.load_test_spec('logging_task') + rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers()) + self.assertEqual([], rv.json) + + def test_add_log(self): + workflow = self.create_workflow('logging_task') + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + log_id = task.data['log_model']['id'] + log_model = session.query(TaskLogModel).filter(TaskLogModel.id == log_id).first() + + self.assertEqual('test_code', log_model.code) + self.assertEqual('info', log_model.level) + self.assertEqual('Activity_LogEvent', log_model.task) + + def test_get_logging_validation(self): + self.load_example_data() + spec_model = self.load_test_spec('get_logging') + rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers()) + self.assertEqual([], rv.json) + + def test_get_logs(self): + workflow = self.create_workflow('get_logging') + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + self.assertEqual(2, len(task.data['logging_models_all_post'])) + self.assertEqual(1, len(task.data['logging_models_info_post'])) + self.assertEqual(1, len(task.data['logging_models_debug_post'])) + self.assertIn(task.data['logging_models_info_post'][0], task.data['logging_models_all_post']) + self.assertIn(task.data['logging_models_debug_post'][0], task.data['logging_models_all_post']) + self.assertEqual('test_code', task.data['logging_models_info_post'][0]['code']) + self.assertEqual('debug_test_code', task.data['logging_models_debug_post'][0]['code']) + + def test_get_logs_for_study(self): + self.load_example_data() + study = session.query(StudyModel).first() + + workflow = self.create_workflow('hello_world', study=study) + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + task_model = Task(id=task.id, + name=task.name, + title=task.title, + type=task.type, + state=task.state, + lane=task.lane, + form=task.form, + documentation=task.documentation, + data=task.data, + multi_instance_type=task.multi_instance_type, + multi_instance_count=task.multi_instance_count, + multi_instance_index=task.multi_instance_index, + process_name=task.process_name, + properties=task.properties) + + task_model.get_name = types.MethodType(lambda x: x.name, task_model) + + TaskLog().do_task(task_model, study.id, workflow.id, + level='critical', + code='critical_code', + message='This is my critical message.') + + TaskLog().do_task(task_model, study.id, workflow.id, + level='debug', + code='debug_code', + message='This is my debug message.') + + # This workflow adds 3 logs + # some_text = 'variable' + # log('info', 'some_code', 'Some longer message') + # log('info', 'some_other_code', 'Another really long message') + # log('debug', 'debug_code', f'This message has a { some_text }!') + workflow = self.create_workflow('get_logging_for_study', study=study) + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + workflow_api = self.complete_form(workflow, task, {}) + task = workflow_api.next_task + workflow_logs = task.data['workflow_logs'] + study_logs = task.data['study_logs'] + self.assertEqual(3, len(workflow_logs)) + self.assertEqual(5, len(study_logs)) diff --git a/tests/workflow/test_workflow_value_expression.py b/tests/workflow/test_workflow_value_expression.py index 1be42451..af8f8426 100644 --- a/tests/workflow/test_workflow_value_expression.py +++ b/tests/workflow/test_workflow_value_expression.py @@ -3,6 +3,7 @@ from tests.base_test import BaseTest class TestValueExpression(BaseTest): + # If there is no default value, a value of 'None' should be given. def test_value_expression_no_default(self): workflow = self.create_workflow('test_value_expression') @@ -14,7 +15,7 @@ class TestValueExpression(BaseTest): workflow_api = self.get_workflow_api(workflow) second_task = workflow_api.next_task self.assertEqual('', second_task.data['value_expression_value']) - self.assertNotIn('color', second_task.data) + self.assertIn('color', second_task.data)