diff --git a/requirements/dev.txt b/requirements/dev.txt index 96e7cc398..418255ce9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -16,8 +16,7 @@ defusedxml==0.5.0 # via python3-openid, social-auth-core dj-database-url==0.4.2 django-braces==1.11.0 django-guardian==1.4.9 -django==1.11.27 -first==2.0.2 # via pip-tools +django==1.11.28 flake8==3.4.1 gitdb2==2.0.5 # via gitpython gitpython==2.1.7 diff --git a/requirements/test.txt b/requirements/test.txt index 14a5a997f..ee8cbb947 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -28,8 +28,8 @@ pycodestyle==2.5.0 # via flake8 pyflakes==2.1.1 # via flake8 pyjwt==1.7.1 # via social-auth-core pytest-cov==2.5.1 -pytest-django==3.1.2 -pytest==3.2.0 +pytest-django==3.8.0 +pytest==3.6.0 python3-openid==3.1.0 # via social-auth-core pytz==2018.9 # via django requests-oauthlib==1.2.0 # via social-auth-core diff --git a/weblab/conftest.py b/weblab/conftest.py index 39ba2f6d8..77197e12e 100644 --- a/weblab/conftest.py +++ b/weblab/conftest.py @@ -177,7 +177,7 @@ def queued_experiment(model_with_version, protocol_with_version): experiment__protocol=protocol_with_version, experiment__protocol_version=protocol_with_version.repocache.latest_version, ) - recipes.running_experiment.make(experiment_version=version) + recipes.running_experiment.make(runnable=version) return version diff --git a/weblab/core/recipes.py b/weblab/core/recipes.py index dc8715f20..65d75ca91 100644 --- a/weblab/core/recipes.py +++ b/weblab/core/recipes.py @@ -1,19 +1,19 @@ from model_mommy.recipe import Recipe, foreign_key, seq -user = Recipe('accounts.User', institution='UCL') +user = Recipe('accounts.User', institution='UCL', full_name=seq('test user ')) model = Recipe( 'ModelEntity', - entity_type='model', name=seq('mymodel') + entity_type='model', name=seq('my model') ) protocol = Recipe( 'ProtocolEntity', - entity_type='protocol', name=seq('myprotocol') + entity_type='protocol', name=seq('my protocol') ) fittingspec = Recipe( 'FittingSpec', - entity_type='fittingspec', name=seq('myspec'), + entity_type='fittingspec', name=seq('my spec'), protocol=foreign_key(protocol), ) @@ -38,13 +38,15 @@ protocol_version=foreign_key(cached_protocol_version), ) +runnable = Recipe('Runnable') + experiment_version = Recipe('ExperimentVersion', experiment=foreign_key(experiment)) -running_experiment = Recipe('RunningExperiment', experiment_version=foreign_key(experiment_version)) +running_experiment = Recipe('RunningExperiment', runnable=foreign_key(runnable)) dataset = Recipe('Dataset', - name=seq('mydataset'), + name=seq('my dataset'), protocol=foreign_key(protocol)) dataset_file = Recipe('DatasetFile', diff --git a/weblab/entities/tests/test_views.py b/weblab/entities/tests/test_views.py index 0177e8120..e38f454f0 100644 --- a/weblab/entities/tests/test_views.py +++ b/weblab/entities/tests/test_views.py @@ -431,10 +431,10 @@ def test_complex_visibilities(self, client, logged_in_user, other_user, helpers) assert len(interfaces) == 4 expected = { - 'myprotocol1': {'required': ['p1r2'], 'optional': ['p1o2']}, - 'myprotocol2': {'required': ['p2r2'], 'optional': ['p2o2']}, - 'myprotocol3': {'required': ['p3r1'], 'optional': ['p3o1']}, - 'myprotocol4': {'required': ['p4r3'], 'optional': ['p4o3']}, + protocol1.name: {'required': ['p1r2'], 'optional': ['p1o2']}, + protocol2.name: {'required': ['p2r2'], 'optional': ['p2o2']}, + protocol3.name: {'required': ['p3r1'], 'optional': ['p3o1']}, + protocol4.name: {'required': ['p4r3'], 'optional': ['p4o3']}, } for iface in interfaces: assert iface['name'] in expected @@ -483,7 +483,7 @@ def test_applies_visibility(self, client, helpers, experiment_version): exp.model.set_version_visibility('latest', 'public') exp.protocol.set_version_visibility('latest', 'public') protocol_version = helpers.add_version(protocol, visibility='private') - recipes.experiment_version.make( + recipes.runnable.make( experiment__protocol=protocol, experiment__protocol_version=protocol.repocache.get_version(protocol_version.sha), experiment__model=exp.model, @@ -1666,7 +1666,7 @@ def test_download_archive(self, client, helpers): archive = zipfile.ZipFile(BytesIO(response.content)) assert archive.filelist[0].filename == 'file1.txt' assert response['Content-Disposition'] == ( - 'attachment; filename=%s_%s.zip' % (model.name, commit.sha) + 'attachment; filename=%s_%s.zip' % (model.name.replace(' ', '_'), commit.sha) ) def test_returns_404_if_no_commits_yet(self, logged_in_user, client): @@ -2115,7 +2115,7 @@ def test_view_run_experiment_model(self, client, helpers, logged_in_user): assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2147,14 +2147,14 @@ def test_view_run_experiment_model_multiple_users(self, client, helpers, logged_ assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] assert response.context['other_object_list'] == [ {'id': other_protocol.pk, 'entity': other_protocol, - 'name': 'myprotocol2', + 'name': other_protocol.name, 'versions': [{'commit': other_version2, 'tags': ['v1'], 'latest': True}, {'commit': other_version1, 'tags': [], 'latest': False}]}, ] @@ -2179,7 +2179,7 @@ def test_view_run_experiment_model_post(self, client, helpers, logged_in_user): assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2229,7 +2229,7 @@ def test_view_run_experiment_model_post_exclude_existing(self, client, helpers, assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2281,14 +2281,14 @@ def test_view_run_experiment_post_model_multiple_users(self, client, helpers, lo assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] assert response.context['other_object_list'] == [ {'id': other_protocol.pk, 'entity': other_protocol, - 'name': 'myprotocol2', + 'name': other_protocol.name, 'versions': [{'commit': other_version2, 'tags': ['v1'], 'latest': True}, {'commit': other_version1, 'tags': [], 'latest': False}]}, ] @@ -2338,7 +2338,7 @@ def test_view_run_experiment_model_not_latest(self, client, helpers, logged_in_u assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2384,7 +2384,7 @@ def test_view_run_experiment_protocol(self, client, helpers, logged_in_user): assert response.status_code == 200 assert response.context['object_list'] == [{'id': model.pk, 'entity': model, - 'name': 'mymodel1', + 'name': model.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2416,14 +2416,14 @@ def test_view_run_experiment_protocol_multiple_users(self, client, helpers, logg assert response.status_code == 200 assert response.context['object_list'] == [{'id': model.pk, 'entity': model, - 'name': 'mymodel1', + 'name': model.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] assert response.context['other_object_list'] == [ {'id': other_model.pk, 'entity': other_model, - 'name': 'mymodel2', + 'name': other_model.name, 'versions': [{'commit': other_version2, 'tags': ['v1'], 'latest': True}, {'commit': other_version1, 'tags': [], 'latest': False}]}, ] @@ -2446,7 +2446,7 @@ def test_view_run_experiment_protocol_post(self, client, helpers, logged_in_user assert response.status_code == 200 assert response.context['object_list'] == [{'id': model.pk, 'entity': model, - 'name': 'mymodel1', + 'name': model.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2502,7 +2502,7 @@ def test_view_run_experiment_protocol_post_exclude_existing(self, client, helper assert response.status_code == 200 assert response.context['object_list'] == [{'id': model.pk, 'entity': model, - 'name': 'mymodel1', + 'name': model.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2551,14 +2551,14 @@ def test_view_run_experiment_post_protocol_multiple_users(self, client, helpers, assert response.status_code == 200 assert response.context['object_list'] == [{'id': model.pk, 'entity': model, - 'name': 'mymodel1', + 'name': model.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] assert response.context['other_object_list'] == [ {'id': other_model.pk, 'entity': other_model, - 'name': 'mymodel2', + 'name': other_model.name, 'versions': [{'commit': other_version2, 'tags': ['v1'], 'latest': True}, {'commit': other_version1, 'tags': [], 'latest': False}]}, ] @@ -2605,7 +2605,7 @@ def test_view_run_experiment_none_checked(self, client, helpers, logged_in_user) assert response.status_code == 200 assert response.context['object_list'] == [{'id': protocol.pk, 'entity': protocol, - 'name': 'myprotocol1', + 'name': protocol.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] @@ -2653,7 +2653,7 @@ def test_view_run_experiment_protocol_not_latest(self, client, helpers, logged_i assert response.status_code == 200 assert response.context['object_list'] == [{'id': model.pk, 'entity': model, - 'name': 'mymodel1', + 'name': model.name, 'versions': [{'commit': version2, 'tags': ['v1'], 'latest': True}, {'commit': version1, 'tags': [], 'latest': False}]}, ] diff --git a/weblab/entities/views.py b/weblab/entities/views.py index 765b3738e..8cf72e589 100644 --- a/weblab/entities/views.py +++ b/weblab/entities/views.py @@ -663,7 +663,7 @@ def check_access_token(self, token): """ from entities.models import AnalysisTask from experiments.models import RunningExperiment - entity_field = 'experiment_version__experiment__%s' % self.kwargs['entity_type'] + entity_field = 'runnable__experimentversion__experiment__%s' % self.kwargs['entity_type'] self_id = self._get_object().id return (RunningExperiment.objects.filter( id=token, diff --git a/weblab/experiments/apps.py b/weblab/experiments/apps.py index 03d41a9fe..3775bf0ce 100644 --- a/weblab/experiments/apps.py +++ b/weblab/experiments/apps.py @@ -1,14 +1,14 @@ from django.apps import AppConfig from django.db.models.signals import pre_delete -from .signals import experiment_version_deleted, running_experiment_deleted +from .signals import runnable_deleted, running_experiment_deleted class ExperimentsConfig(AppConfig): name = 'experiments' def ready(self): - from .models import ExperimentVersion, RunningExperiment + from .models import Runnable, RunningExperiment - pre_delete.connect(experiment_version_deleted, ExperimentVersion) + pre_delete.connect(runnable_deleted, Runnable) pre_delete.connect(running_experiment_deleted, RunningExperiment) diff --git a/weblab/experiments/emails.py b/weblab/experiments/emails.py index c7cc10c1e..ef20b7662 100644 --- a/weblab/experiments/emails.py +++ b/weblab/experiments/emails.py @@ -3,15 +3,15 @@ from django.template.loader import render_to_string -def send_experiment_finished_email(experiment_version): - author = experiment_version.author +def send_experiment_finished_email(runnable): + author = runnable.author if author.receive_emails: body = render_to_string( 'emails/experiment_finished.txt', { 'user': author, - 'experiment_version': experiment_version, + 'runnable': runnable, 'base_url': settings.BASE_URL, } ) diff --git a/weblab/experiments/migrations/0025_auto_20200316_1652.py b/weblab/experiments/migrations/0025_auto_20200316_1652.py new file mode 100644 index 000000000..43e9d91a0 --- /dev/null +++ b/weblab/experiments/migrations/0025_auto_20200316_1652.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 16:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0024_merge_20200211_0929'), + ] + + operations = [ + migrations.RenameModel( + old_name='ExperimentVersion', + new_name='Runnable', + ), + migrations.RemoveIndex( + model_name='runnable', + name='experiments_created_00e2f1_idx', + ), + migrations.AddIndex( + model_name='runnable', + index=models.Index(fields=['created_at'], name='experiments_created_a4e4b7_idx'), + ), + ] diff --git a/weblab/experiments/migrations/0026_experimentversion.py b/weblab/experiments/migrations/0026_experimentversion.py new file mode 100644 index 000000000..e0913ae0a --- /dev/null +++ b/weblab/experiments/migrations/0026_experimentversion.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 17:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0025_auto_20200316_1652'), + ] + + operations = [ + migrations.CreateModel( + name='ExperimentVersion', + fields=[ + ('runnable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='experiments.Runnable')), + ('experiment_key', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='versions_copy', to='experiments.Experiment')), + ], + options={ + 'abstract': False, + }, + bases=('experiments.runnable',), + ), + ] diff --git a/weblab/experiments/migrations/0027_move_runnable.py b/weblab/experiments/migrations/0027_move_runnable.py new file mode 100644 index 000000000..4389faa89 --- /dev/null +++ b/weblab/experiments/migrations/0027_move_runnable.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 17:02 +from __future__ import unicode_literals + +from django.db import migrations + + + +def runnable_to_exp_version(apps, schema_editor): + ExperimentVersion = apps.get_model('experiments', 'ExperimentVersion') + Runnable = apps.get_model('experiments', 'Runnable') + for runnable in Runnable.objects.all(): + experiment_version = ExperimentVersion(runnable_ptr=runnable, + experiment_key_id=runnable.experiment_id) + experiment_version.save_base(raw=True) + + +def exp_version_to_runnable(apps, schema_editor): + ExperimentVersion = apps.get_model('experiments', 'ExperimentVersion') + Runnable = apps.get_model('experiments', 'Runnable') + for experiment_version in ExperimentVersion.objects.all(): + runnable = experiment_version.runnable_ptr + runnable.experiment_id = experiment_version.experiment_key_id + runnable.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0026_experimentversion'), + ] + + operations = [ + migrations.RunPython(runnable_to_exp_version, exp_version_to_runnable), + ] diff --git a/weblab/experiments/migrations/0028_auto_20200316_1706.py b/weblab/experiments/migrations/0028_auto_20200316_1706.py new file mode 100644 index 000000000..016d1ebda --- /dev/null +++ b/weblab/experiments/migrations/0028_auto_20200316_1706.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 17:06 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0027_move_runnable'), + ] + + operations = [ + migrations.AlterField( + model_name='runnable', + name='experiment', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='versions', to='experiments.Experiment'), + ), + ] diff --git a/weblab/experiments/migrations/0029_remove_runnable_experiment.py b/weblab/experiments/migrations/0029_remove_runnable_experiment.py new file mode 100644 index 000000000..482808398 --- /dev/null +++ b/weblab/experiments/migrations/0029_remove_runnable_experiment.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 17:06 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0028_auto_20200316_1706'), + ] + + operations = [ + migrations.RemoveField( + model_name='runnable', + name='experiment', + ), + ] diff --git a/weblab/experiments/migrations/0030_auto_20200316_1707.py b/weblab/experiments/migrations/0030_auto_20200316_1707.py new file mode 100644 index 000000000..7b35ed1e9 --- /dev/null +++ b/weblab/experiments/migrations/0030_auto_20200316_1707.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 17:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0029_remove_runnable_experiment'), + ] + + operations = [ + migrations.RenameField( + model_name='experimentversion', + old_name='experiment_key', + new_name='experiment', + ), + ] diff --git a/weblab/experiments/migrations/0031_auto_20200316_1708.py b/weblab/experiments/migrations/0031_auto_20200316_1708.py new file mode 100644 index 000000000..80a0bee59 --- /dev/null +++ b/weblab/experiments/migrations/0031_auto_20200316_1708.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-16 17:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0030_auto_20200316_1707'), + ] + + operations = [ + migrations.AlterField( + model_name='experimentversion', + name='experiment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='experiments.Experiment'), + ), + ] diff --git a/weblab/experiments/migrations/0032_auto_20200317_0927.py b/weblab/experiments/migrations/0032_auto_20200317_0927.py new file mode 100644 index 000000000..42cba7888 --- /dev/null +++ b/weblab/experiments/migrations/0032_auto_20200317_0927.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-03-17 09:27 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0031_auto_20200316_1708'), + ] + + operations = [ + migrations.RenameField( + model_name='runningexperiment', + old_name='experiment_version', + new_name='runnable', + ), + ] diff --git a/weblab/experiments/models.py b/weblab/experiments/models.py index 589d81677..fcd12f954 100644 --- a/weblab/experiments/models.py +++ b/weblab/experiments/models.py @@ -106,11 +106,16 @@ def nice_protocol_version(self): def latest_result(self): try: return self.latest_version.status - except ExperimentVersion.DoesNotExist: + except Runnable.DoesNotExist: return '' -class ExperimentVersion(UserCreatedModelMixin, FileCollectionMixin, models.Model): +class Runnable(UserCreatedModelMixin, FileCollectionMixin, models.Model): + """ Runnable base class + Represents experiments and fitting specs that have the facility to + run on the back-end, + The current status of the run is recorded as well as the and results when completed. + """ STATUS_QUEUED = "QUEUED" STATUS_RUNNING = "RUNNING" STATUS_SUCCESS = "SUCCESS" @@ -127,7 +132,6 @@ class ExperimentVersion(UserCreatedModelMixin, FileCollectionMixin, models.Model (STATUS_INAPPLICABLE, STATUS_INAPPLICABLE), ) - experiment = models.ForeignKey(Experiment, related_name='versions') finished_at = models.DateTimeField(null=True, blank=True) status = models.CharField( max_length=16, @@ -197,13 +201,29 @@ def update(self, status, txt): self.save() -class RunningExperiment(models.Model): +class ExperimentVersion(Runnable): + """ ExperimentVersion class + This records a single run of a particular Experiment. + The same model/protocol combination may be run more than once, + resulting in an Experiment having multiple versions. """ - A current run of an ExperimentVersion + experiment = models.ForeignKey(Experiment, related_name='versions') + + @property + def parent(self): + """The Experiment this is a version of.""" + return self.experiment + + +class RunningExperiment(models.Model): + """ Class to track an in-progress Runnable instance. + It adds functionality to link to the task id on the back-end system, so that returned results + can be linked to the appropriate Runnable. + The running tasks can be cancelled by deleting using the front-end. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - experiment_version = models.ForeignKey(ExperimentVersion, related_name='running') + runnable = models.ForeignKey(Runnable, related_name='running') task_id = models.CharField(max_length=50) diff --git a/weblab/experiments/processing.py b/weblab/experiments/processing.py index 92b2f8607..40c8c62f9 100644 --- a/weblab/experiments/processing.py +++ b/weblab/experiments/processing.py @@ -9,7 +9,12 @@ from django.utils.timezone import now from .emails import send_experiment_finished_email -from .models import Experiment, ExperimentVersion, RunningExperiment +from .models import ( + Experiment, + ExperimentVersion, + Runnable, + RunningExperiment, +) logger = logging.getLogger(__name__) @@ -23,11 +28,11 @@ class ChasteProcessingStatus: INAPPLICABLE = "inapplicable" MODEL_STATUSES = { - SUCCESS: ExperimentVersion.STATUS_SUCCESS, - RUNNING: ExperimentVersion.STATUS_RUNNING, - PARTIAL: ExperimentVersion.STATUS_PARTIAL, - FAILED: ExperimentVersion.STATUS_FAILED, - INAPPLICABLE: ExperimentVersion.STATUS_INAPPLICABLE, + SUCCESS: Runnable.STATUS_SUCCESS, + RUNNING: Runnable.STATUS_RUNNING, + PARTIAL: Runnable.STATUS_PARTIAL, + FAILED: Runnable.STATUS_FAILED, + INAPPLICABLE: Runnable.STATUS_INAPPLICABLE, } @classmethod @@ -75,7 +80,7 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re author=user, ) - run = RunningExperiment.objects.create(experiment_version=version) + run = RunningExperiment.objects.create(runnable=version) signature = version.signature model_url = reverse( @@ -102,7 +107,7 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re response = requests.post(settings.CHASTE_URL, body) except requests.exceptions.ConnectionError: run.delete() - version.status = ExperimentVersion.STATUS_FAILED + version.status = Runnable.STATUS_FAILED version.return_text = 'Unable to connect to experiment runner service' version.save() logger.exception(version.return_text) @@ -113,7 +118,7 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re if not res.startswith(signature): run.delete() - version.status = ExperimentVersion.STATUS_FAILED + version.status = Runnable.STATUS_FAILED version.return_text = 'Chaste backend answered with something unexpected: %s' % res version.save() logger.error(version.return_text) @@ -126,11 +131,11 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re run.save() elif status == 'inapplicable': run.delete() - version.status = ExperimentVersion.STATUS_INAPPLICABLE + version.status = Runnable.STATUS_INAPPLICABLE else: run.delete() logger.error('Chaste backend answered with error: %s' % status) - version.status = ExperimentVersion.STATUS_FAILED + version.status = Runnable.STATUS_FAILED version.return_text = status version.save() @@ -161,7 +166,7 @@ def process_callback(data, files): try: run = RunningExperiment.objects.get(id=signature) - exp = run.experiment_version + exp = run.runnable except RunningExperiment.DoesNotExist: return {'error': 'invalid signature'} @@ -187,7 +192,7 @@ def process_callback(data, files): exp.save() - if exp.is_finished or exp.status == ExperimentVersion.STATUS_INAPPLICABLE: + if exp.is_finished or exp.status == Runnable.STATUS_INAPPLICABLE: # We unset the task_id to ensure the delete() below doesn't send a message to the back-end cancelling # the task, causing it to be killed while still sending us its 'finished' message! run.task_id = '' @@ -197,7 +202,7 @@ def process_callback(data, files): send_experiment_finished_email(exp) if not files.get('experiment'): - exp.update(ExperimentVersion.STATUS_FAILED, + exp.update(Runnable.STATUS_FAILED, '%s (backend returned no archive)' % exp.return_text) return {'error': 'no archive found'} @@ -210,7 +215,7 @@ def process_callback(data, files): try: zipfile.ZipFile(str(exp.archive_path)) except zipfile.BadZipFile as e: - exp.update(ExperimentVersion.STATUS_FAILED, 'error reading archive: %s' % e) + exp.update(Runnable.STATUS_FAILED, 'error reading archive: %s' % e) return {'experiment': 'failed'} return {'experiment': 'ok'} diff --git a/weblab/experiments/signals.py b/weblab/experiments/signals.py index b35a7ce53..befb51892 100644 --- a/weblab/experiments/signals.py +++ b/weblab/experiments/signals.py @@ -1,7 +1,7 @@ from shutil import rmtree -def experiment_version_deleted(sender, instance, **kwargs): +def runnable_deleted(sender, instance, **kwargs): """ Signal callback when an experiment version is about to be deleted. diff --git a/weblab/experiments/tests/test_models.py b/weblab/experiments/tests/test_models.py index e83ff92fd..d9453a1fd 100644 --- a/weblab/experiments/tests/test_models.py +++ b/weblab/experiments/tests/test_models.py @@ -147,7 +147,7 @@ def test_archive_path(self, fake_experiment_path): def test_signature(self): running = recipes.running_experiment.make() - assert running.experiment_version.signature == str(running.id) + assert running.runnable.signature == str(running.id) @pytest.mark.parametrize('status, is_running', [ ('QUEUED', False), diff --git a/weblab/experiments/tests/test_views.py b/weblab/experiments/tests/test_views.py index a60c0d1d4..98109c57c 100644 --- a/weblab/experiments/tests/test_views.py +++ b/weblab/experiments/tests/test_views.py @@ -13,6 +13,7 @@ from django.core.urlresolvers import reverse from django.test import Client from django.utils.dateparse import parse_datetime +from pytest_django.asserts import assertContains, assertTemplateUsed from core import recipes from experiments.models import ( @@ -950,8 +951,9 @@ def test_view_experiment_version(self, client, experiment_version): experiment_version.pk)) ) - assert response.status_code == 200 assert response.context['version'] == experiment_version + assertTemplateUsed(response, 'experiments/experimentversion_detail.html') + assertContains(response, 'Download archive of all files') @pytest.mark.django_db @@ -970,7 +972,7 @@ def test_load_page_logged_in_user(self, client): def test_get_queryset_other_user(self, other_user, client, experiment_version): experiment_version.author = other_user experiment_version.save() - recipes.running_experiment.make(experiment_version=experiment_version) + recipes.running_experiment.make(runnable=experiment_version) assert RunningExperiment.objects.count() == 1 response = client.get('/experiments/tasks') assert len(response.context['runningexperiment_list']) == 0 @@ -1002,7 +1004,7 @@ def test_get_queryset(self, logged_in_user, client, helpers): experiment__protocol_version=protocol_1.repocache.get_version(protocol_1_version2.sha), author=logged_in_user, ) - running_exp_version2 = recipes.running_experiment.make(experiment_version=exp_version_2) + running_exp_version2 = recipes.running_experiment.make(runnable=exp_version_2) exp_version_3 = recipes.experiment_version.make( status=ExperimentVersion.STATUS_RUNNING, @@ -1012,7 +1014,7 @@ def test_get_queryset(self, logged_in_user, client, helpers): experiment__protocol_version=protocol_2.repocache.get_version(protocol_2_version.sha), author=logged_in_user, ) - running_exp_version3 = recipes.running_experiment.make(experiment_version=exp_version_3) + running_exp_version3 = recipes.running_experiment.make(runnable=exp_version_3) assert ExperimentVersion.objects.count() == 3 assert RunningExperiment.objects.count() == 2 @@ -1022,7 +1024,7 @@ def test_get_queryset(self, logged_in_user, client, helpers): assert set(response.context['runningexperiment_list']) == {running_exp_version2, running_exp_version3} # Cancel one of the running versions check the other one is still present - client.post('/experiments/tasks', {'chkBoxes[]': [running_exp_version2.experiment_version.id]}) + client.post('/experiments/tasks', {'chkBoxes[]': [running_exp_version2.runnable.id]}) response = client.get('/experiments/tasks') assert set(response.context['runningexperiment_list']) == {running_exp_version3} assert RunningExperiment.objects.count() == 1 @@ -1031,9 +1033,9 @@ def test_get_queryset(self, logged_in_user, client, helpers): def test_returns_404_incorrect_owner(self, other_user, client, experiment_version): experiment_version.author = other_user experiment_version.save() - running_exp = recipes.running_experiment.make(experiment_version=experiment_version) + running_exp = recipes.running_experiment.make(runnable=experiment_version) assert RunningExperiment.objects.count() == 1 - response = client.post('/experiments/tasks', {'chkBoxes[]': [running_exp.experiment_version.id]}) + response = client.post('/experiments/tasks', {'chkBoxes[]': [running_exp.runnable.id]}) assert RunningExperiment.objects.count() == 1 assert response.status_code == 404 diff --git a/weblab/experiments/views.py b/weblab/experiments/views.py index 047aa7929..0514d8784 100644 --- a/weblab/experiments/views.py +++ b/weblab/experiments/views.py @@ -52,10 +52,11 @@ class ExperimentTasks(LoginRequiredMixin, ListView): def get_queryset(self): return RunningExperiment.objects.filter( - experiment_version__author=self.request.user + runnable__author=self.request.user ).order_by( - 'experiment_version__created_at', - ).select_related('experiment_version', 'experiment_version__experiment') + 'runnable__created_at', + ).select_related('runnable', + 'runnable__experimentversion__experiment') def post(self, request): for running_exp_id in request.POST.getlist('chkBoxes[]'): diff --git a/weblab/templates/emails/experiment_finished.txt b/weblab/templates/emails/experiment_finished.txt index 6a2240b64..f1e7b9900 100644 --- a/weblab/templates/emails/experiment_finished.txt +++ b/weblab/templates/emails/experiment_finished.txt @@ -2,9 +2,9 @@ Hi {{ user.full_name }} An experiment you submitted has finished. -Status: {{ experiment_version.status }} +Status: {{ runnable.status }} -The resulting files can be viewed at: {{ base_url }}{% url 'experiments:version' experiment_version.experiment.id experiment_version.id %} +The resulting files can be viewed at: {{ base_url }}{% url 'experiments:version' runnable.experimentversion.experiment.id runnable.id %} Your sincerely, Cardiac Web Lab website diff --git a/weblab/templates/experiments/experiment_tasks.html b/weblab/templates/experiments/experiment_tasks.html index 7bb5aced8..09325ef9c 100644 --- a/weblab/templates/experiments/experiment_tasks.html +++ b/weblab/templates/experiments/experiment_tasks.html @@ -33,18 +33,20 @@