diff --git a/.gitignore b/.gitignore index 14f565d1b..6d1dee062 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,9 @@ ENV/ # Rope project settings .ropeproject +# PyCharm files +.idea/ + # mkdocs documentation /site @@ -113,5 +116,3 @@ weblab/data/ # media directory weblab/media/ - -weblab/\.idea/ diff --git a/weblab/entities/templatetags/entities.py b/weblab/entities/templatetags/entities.py index df69d44fa..76658fd09 100644 --- a/weblab/entities/templatetags/entities.py +++ b/weblab/entities/templatetags/entities.py @@ -189,3 +189,10 @@ def can_delete_entity(context, entity): def can_manage_entity(context, entity): user = context['user'] return entity.is_managed_by(user) + + +@register.filter +def url_run_experiments(entity, commit): + last_tag = _url_friendly_label(entity, commit) + args = [entity.entity_type, entity.id, last_tag] + return reverse('entities:runexperiments', args=args) diff --git a/weblab/entities/tests/test_templatetags.py b/weblab/entities/tests/test_templatetags.py index 464e23878..dd6e3a8a1 100644 --- a/weblab/entities/tests/test_templatetags.py +++ b/weblab/entities/tests/test_templatetags.py @@ -123,3 +123,16 @@ def test_url_friendly_label(model_with_version, helpers): commit3 = helpers.add_version(model_with_version) model_with_version.repo.tag('latest') assert entity_tags._url_friendly_label(model_with_version, commit3) == commit3.hexsha + + +@pytest.mark.django_db +def test_url_runexperiments(model_with_version, protocol_with_version): + model = model_with_version + model_commit = model.repo.latest_commit + assert (entity_tags.url_run_experiments(model, model_commit) == + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, model_commit.hexsha)) + + protocol = protocol_with_version + protocol_commit = protocol.repo.latest_commit + assert (entity_tags.url_run_experiments(protocol, protocol_commit) == + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, protocol_commit.hexsha)) diff --git a/weblab/entities/tests/test_views.py b/weblab/entities/tests/test_views.py index 47a4225f0..793faed7b 100644 --- a/weblab/entities/tests/test_views.py +++ b/weblab/entities/tests/test_views.py @@ -2055,3 +2055,509 @@ def test_nonexistent_entity_redirects_anonymous_to_login(self, client, helpers, def test_nonexistent_entity_generates_404_for_user(self, client, logged_in_user, helpers, recipe, url): response = client.get(url % 10000) assert response.status_code == 404 + + +@pytest.mark.django_db +class TestEntityRunExperiment: + def test_view_run_experiment_model(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + helpers.add_version(model, visibility='private') + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, 'latest')) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['preposition'] == 'under' + + def test_view_run_experiment_model_multiple_users(self, client, helpers, logged_in_user, other_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + helpers.add_version(model, visibility='moderated') + + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + other_protocol = recipes.protocol.make(author=other_user) + other_commit1 = helpers.add_version(other_protocol, visibility='public') + other_commit2 = helpers.add_version(other_protocol, visibility='public') + other_protocol.add_tag('v1', other_commit2.hexsha) + + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, 'latest')) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['other_object_list'] == [{'id': other_protocol.pk, + 'name': 'myprotocol2', + 'versions': [{'commit': other_commit2, 'tags': ['v1'], 'latest': True}, + {'commit': other_commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['preposition'] == 'under' + + def test_view_run_experiment_model_post(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit_model = helpers.add_version(model, visibility='public') + + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + # Test context has correct information + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (protocol.pk, commit2.hexsha)], + 'rerun_expts': 'on'} + response = client.post( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/models/%d/versions/latest' % model.pk + + # Test that planned experiments have been added correctly + expected_proto_versions = set([ + (protocol, commit2.hexsha) + ]) + assert PlannedExperiment.objects.count() == 1 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.model == model + assert planned_experiment.model_version == commit_model.hexsha + assert (planned_experiment.protocol, planned_experiment.protocol_version) in expected_proto_versions + + def test_view_run_experiment_model_post_exclude_existing(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit_model = helpers.add_version(model, visibility='public') + + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + recipes.experiment_version.make( + status='SUCCESS', + experiment__model=model, + experiment__model_version=commit_model.hexsha, + experiment__protocol=protocol, + experiment__protocol_version=commit1.hexsha) + + # Test context has correct information + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (protocol.pk, commit1.hexsha), + '%d:%s' % (protocol.pk, commit2.hexsha)], + } + response = client.post( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/models/%d/versions/latest' % model.pk + + # Test that planned experiments have been added correctly + expected_proto_versions = set([ + (protocol, commit2.hexsha) + ]) + assert PlannedExperiment.objects.count() == 1 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.model == model + assert planned_experiment.model_version == commit_model.hexsha + assert (planned_experiment.protocol, planned_experiment.protocol_version) in expected_proto_versions + + def test_view_run_experiment_post_model_multiple_users(self, client, helpers, logged_in_user, other_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit_model = helpers.add_version(model, visibility='public') + + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + other_protocol = recipes.protocol.make(author=other_user) + other_commit1 = helpers.add_version(other_protocol, visibility='public') + other_commit2 = helpers.add_version(other_protocol, visibility='public') + other_protocol.add_tag('v1', other_commit2.hexsha) + + # check context + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['other_object_list'] == [{'id': other_protocol.pk, + 'name': 'myprotocol2', + 'versions': [{'commit': other_commit2, 'tags': ['v1'], 'latest': True}, + {'commit': other_commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (protocol.pk, commit2.hexsha), + '%d:%s' % (other_protocol.pk, other_commit1.hexsha), + '%d:%s' % (other_protocol.pk, other_commit2.hexsha)], + 'rerun_expts': 'on'} + response = client.post( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/models/%d/versions/latest' % model.pk + + # Test that planned experiments have been added correctly + expected_proto_versions = set([ + (protocol, commit2.hexsha), + (other_protocol, other_commit1.hexsha), + (other_protocol, other_commit2.hexsha) + ]) + assert PlannedExperiment.objects.count() == 3 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.model == model + assert planned_experiment.model_version == commit_model.hexsha + assert (planned_experiment.protocol, planned_experiment.protocol_version) in expected_proto_versions + + def test_view_run_experiment_model_not_latest(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + helpers.add_version(model, visibility='private') + model_commit1 = helpers.add_version(model, visibility='public') + model.add_tag('model_v1', model_commit1.hexsha) + model_commit2 = helpers.add_version(model, visibility='public') + model.add_tag('model_v2', model_commit2.hexsha) + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + # display page using tag + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, 'model_v1')) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['preposition'] == 'under' + + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (protocol.pk, commit2.hexsha), + '%d:%s' % (protocol.pk, commit1.hexsha)], + 'rerun_expts': 'on'} + response = client.post( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, 'model_v1'), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/models/%d/versions/model_v1' % model.pk + + # Test that planned experiments have been added correctly + expected_proto_versions = set([ + (protocol, commit2.hexsha), + (protocol, commit1.hexsha), + ]) + assert PlannedExperiment.objects.count() == 2 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.model == model + assert planned_experiment.model_version == model_commit1.hexsha + assert (planned_experiment.protocol, planned_experiment.protocol_version) in expected_proto_versions + + # repeat tests with protocol as the calling entity + def test_view_run_experiment_protocol(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit1 = helpers.add_version(model, visibility='public') + commit2 = helpers.add_version(model, visibility='public') + model.add_tag('v1', commit2.hexsha) + protocol = recipes.protocol.make(author=logged_in_user) + helpers.add_version(protocol, visibility='private') + + response = client.get( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, 'latest')) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': model.pk, + 'name': 'mymodel1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['preposition'] == 'on' + + def test_view_run_experiment_protocol_multiple_users(self, client, helpers, logged_in_user, other_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit1 = helpers.add_version(model, visibility='public') + commit2 = helpers.add_version(model, visibility='public') + model.add_tag('v1', commit2.hexsha) + + other_model = recipes.model.make(author=other_user) + other_commit1 = helpers.add_version(other_model, visibility='public') + other_commit2 = helpers.add_version(other_model, visibility='public') + other_model.add_tag('v1', other_commit2.hexsha) + + protocol = recipes.protocol.make(author=logged_in_user) + helpers.add_version(protocol, visibility='private') + + response = client.get( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, 'latest')) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': model.pk, + 'name': 'mymodel1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['other_object_list'] == [{'id': other_model.pk, + 'name': 'mymodel2', + 'versions': [{'commit': other_commit2, 'tags': ['v1'], 'latest': True}, + {'commit': other_commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['preposition'] == 'on' + + def test_view_run_experiment_protocol_post(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit1 = helpers.add_version(model, visibility='public') + commit2 = helpers.add_version(model, visibility='public') + model.add_tag('v1', commit2.hexsha) + protocol = recipes.protocol.make(author=logged_in_user) + commit_protocol = helpers.add_version(protocol, visibility='public') + + response = client.get( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, commit_protocol.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': model.pk, + 'name': 'mymodel1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (model.pk, commit1.hexsha), '%d:%s' % (model.pk, commit2.hexsha)], + 'rerun_expts': 'on'} + response = client.post( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, commit_protocol.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/protocols/%d/versions/latest' % protocol.pk + + # Test that planned experiments have been added correctly + expected_model_versions = set([ + (model, commit2.hexsha), + (model, commit1.hexsha) + ]) + assert PlannedExperiment.objects.count() == 2 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.protocol == protocol + assert planned_experiment.protocol_version == commit_protocol.hexsha + assert (planned_experiment.model, planned_experiment.model_version) in expected_model_versions + + def test_view_run_experiment_protocol_post_exclude_existing(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit1 = helpers.add_version(model, visibility='public') + commit2 = helpers.add_version(model, visibility='public') + model.add_tag('v1', commit2.hexsha) + protocol = recipes.protocol.make(author=logged_in_user) + commit_protocol = helpers.add_version(protocol, visibility='public') + + recipes.experiment_version.make( + status='SUCCESS', + experiment__model=model, + experiment__model_version=commit1.hexsha, + experiment__protocol=protocol, + experiment__protocol_version=commit_protocol.hexsha) + # This experiment has no versions so should not be excluded + recipes.experiment.make( + model=model, + model_version=commit2.hexsha, + protocol=protocol, + protocol_version=commit_protocol.hexsha) + + # Test context has correct information + response = client.get( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, commit_protocol.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': model.pk, + 'name': 'mymodel1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (model.pk, commit1.hexsha), '%d:%s' % (model.pk, commit2.hexsha)], + } + response = client.post( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, commit_protocol.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/protocols/%d/versions/latest' % protocol.pk + + # Test that planned experiments have been added correctly + expected_model_versions = set([ + (model, commit2.hexsha), + ]) + assert PlannedExperiment.objects.count() == 1 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.protocol == protocol + assert planned_experiment.protocol_version == commit_protocol.hexsha + assert (planned_experiment.model, planned_experiment.model_version) in expected_model_versions + + def test_view_run_experiment_post_protocol_multiple_users(self, client, helpers, logged_in_user, other_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit1 = helpers.add_version(model, visibility='public') + commit2 = helpers.add_version(model, visibility='public') + model.add_tag('v1', commit2.hexsha) + protocol = recipes.protocol.make(author=logged_in_user) + commit_protocol = helpers.add_version(protocol, visibility='public') + + other_model = recipes.model.make(author=other_user) + other_commit1 = helpers.add_version(other_model, visibility='public') + other_commit2 = helpers.add_version(other_model, visibility='public') + other_model.add_tag('v1', other_commit2.hexsha) + + response = client.get( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, commit_protocol.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': model.pk, + 'name': 'mymodel1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['other_object_list'] == [{'id': other_model.pk, + 'name': 'mymodel2', + 'versions': [{'commit': other_commit2, 'tags': ['v1'], 'latest': True}, + {'commit': other_commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (model.pk, commit1.hexsha), + '%d:%s' % (model.pk, commit2.hexsha), + '%d:%s' % (other_model.pk, other_commit1.hexsha)], + 'rerun_expts': 'on'} + response = client.post( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, commit_protocol.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/protocols/%d/versions/latest' % protocol.pk + + # Test that planned experiments have been added correctly + expected_model_versions = set([ + (model, commit2.hexsha), + (model, commit1.hexsha), + (other_model, other_commit1.hexsha) + ]) + assert PlannedExperiment.objects.count() == 3 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.protocol == protocol + assert planned_experiment.protocol_version == commit_protocol.hexsha + assert (planned_experiment.model, planned_experiment.model_version) in expected_model_versions + + def test_view_run_experiment_none_checked(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit_model = helpers.add_version(model, visibility='public') + + protocol = recipes.protocol.make(author=logged_in_user) + commit1 = helpers.add_version(protocol, visibility='public') + commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('v1', commit2.hexsha) + + # Test context has correct information + response = client.get( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': protocol.pk, + 'name': 'myprotocol1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + # Test post returns correct response + data = {'model_protocol_list[]': [], + 'rerun_expts': 'on'} + response = client.post( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/models/%d/versions/latest' % model.pk + + # Test that no planned experiments have been added + assert PlannedExperiment.objects.count() == 0 + + # Try again without re-running + data = {'model_protocol_list[]': []} + response = client.post( + '/entities/models/%d/versions/%s/runexperiments' % (model.pk, commit_model.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/models/%d/versions/latest' % model.pk + + # Test that no planned experiments have been added + assert PlannedExperiment.objects.count() == 0 + + def test_view_run_experiment_protocol_not_latest(self, client, helpers, logged_in_user): + helpers.add_permission(logged_in_user, 'create_experiment', Experiment) + model = recipes.model.make(author=logged_in_user) + commit1 = helpers.add_version(model, visibility='public') + commit2 = helpers.add_version(model, visibility='public') + model.add_tag('v1', commit2.hexsha) + protocol = recipes.protocol.make(author=logged_in_user) + proto_commit1 = helpers.add_version(protocol, visibility='public') + proto_commit2 = helpers.add_version(protocol, visibility='public') + protocol.add_tag('p1', proto_commit1.hexsha) + protocol.add_tag('p2', proto_commit2.hexsha) + + # display using sha + response = client.get( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, proto_commit1.hexsha)) + assert response.status_code == 200 + assert response.context['object_list'] == [{'id': model.pk, + 'name': 'mymodel1', + 'versions': [{'commit': commit2, 'tags': ['v1'], 'latest': True}, + {'commit': commit1, 'tags': [], 'latest': False}]}, + ] + assert response.context['preposition'] == 'on' + + # Test post returns correct response + data = {'model_protocol_list[]': ['%d:%s' % (model.pk, commit1.hexsha), '%d:%s' % (model.pk, commit2.hexsha)], + 'rerun_expts': 'on'} + response = client.post( + '/entities/protocols/%d/versions/%s/runexperiments' % (protocol.pk, proto_commit1.hexsha), + data=data) + assert response.status_code == 302 + assert response.url == '/entities/protocols/%d/versions/%s' % (protocol.pk, proto_commit1.hexsha) + + # Test that planned experiments have been added correctly + expected_model_versions = set([ + (model, commit2.hexsha), + (model, commit1.hexsha) + ]) + assert PlannedExperiment.objects.count() == 2 + for planned_experiment in PlannedExperiment.objects.all(): + assert planned_experiment.protocol == protocol + assert planned_experiment.protocol_version == proto_commit1.hexsha + assert (planned_experiment.model, planned_experiment.model_version) in expected_model_versions + diff --git a/weblab/entities/urls.py b/weblab/entities/urls.py index b14734a58..791916283 100644 --- a/weblab/entities/urls.py +++ b/weblab/entities/urls.py @@ -118,6 +118,12 @@ name='entity_archive', ), + url( + r'^%s/(?P\d+)/versions/%s/runexperiments$' % (_ENTITY_TYPE, _COMMIT), + views.EntityRunExperimentView.as_view(), + name='runexperiments', + ), + url( r'^(?P\d+)/upload-file$', views.FileUploadView.as_view(), diff --git a/weblab/entities/views.py b/weblab/entities/views.py index 663524fb6..c665ab96d 100644 --- a/weblab/entities/views.py +++ b/weblab/entities/views.py @@ -41,7 +41,7 @@ from core.visibility import ( Visibility, VisibilityMixin ) -from experiments.models import Experiment, PlannedExperiment +from experiments.models import Experiment, PlannedExperiment, ExperimentVersion from repocache.exceptions import RepoCacheMiss from repocache.models import CachedEntityVersion @@ -1016,3 +1016,80 @@ def get(self, request, *args, **kwargs): return JsonResponse({ task: result }) + + +class EntityRunExperimentView(PermissionRequiredMixin, LoginRequiredMixin, + EntityTypeMixin, EntityVersionMixin, DetailView): + """ + A view allowing users to set up a batch-run of experiments involving a single entity. + """ + permission_required = 'experiments.create_experiment' + context_object_name = 'entity' + template_name = 'entities/entity_runexperiments.html' + + def get_context_data(self, **kwargs): + entity = self.object + context = super().get_context_data(**kwargs) + + # preposition to use in sentence: You may run this entity on/under the following entities + context['preposition'] = 'under' + if entity.entity_type == 'protocol': + context['preposition'] = 'on' + + # ended up using a nested dict as nested lists caused django's unpacking in forloops to + # mess things up slightly + other_entities = Entity.objects.filter( + entity_type=entity.other_type + ).select_related( + 'cachedentity' + ).prefetch_related( + 'cachedentity__versions', + 'cachedentity__versions__tags' + ) + context['object_list'] = [] + context['other_object_list'] = [] + for item in other_entities: + versions = item.cachedentity.versions + version_info = [] + for version in versions.prefetch_related('tags'): + tag_list = list(version.tags.values_list('tag', flat=True)) + commit = item.repo.get_commit(version.sha) + latest = item.repo.latest_commit + version_info.append({'commit': commit, 'tags': tag_list, 'latest': latest == commit}) + if item.author == self.request.user: + context['object_list'].append({'id': item.id, 'name': item.name, 'versions': version_info}) + else: + context['other_object_list'].append({'id': item.id, 'name': item.name, 'versions': version_info}) + return context + + def post(self, request, *args, **kwargs): + # this in not intuitive + # in get context self.object was the entity being worked with + # here we have to retrieve it + this_entity = self.get_object() + this_version = self.get_commit().hexsha + is_latest = (this_version == this_entity.repocache.latest_version.sha) + exclude_existing = 'rerun_expts' not in request.POST + experiments_to_run = request.POST.getlist('model_protocol_list[]') + for version in experiments_to_run: + ident, sha = version.split(':') + exper_kwargs = { + this_entity.other_type + '_id': ident, + this_entity.other_type + '_version': sha, + this_entity.entity_type + '_id': this_entity.id, + this_entity.entity_type + '_version': this_version, + } + if exclude_existing: + filter_kwargs = { + 'experiment__' + name: value + for (name, value) in exper_kwargs.items() + } + if ExperimentVersion.objects.filter(**filter_kwargs).exists(): + continue + PlannedExperiment.objects.get_or_create(**exper_kwargs) + # return to entity page + version_to_use = 'latest' + if not is_latest: + version_to_use = kwargs['sha'] + return HttpResponseRedirect( + reverse('entities:version', args=[kwargs['entity_type'], kwargs['pk'], version_to_use])) diff --git a/weblab/static/js/main.js b/weblab/static/js/main.js index 15de91e10..9dc2aefaa 100644 --- a/weblab/static/js/main.js +++ b/weblab/static/js/main.js @@ -10,6 +10,7 @@ var experiment = require('./experiment.js'); var notifications = require('./lib/notifications.js'); require('./compare.js'); require('./entity_version_list.js'); +require('./run_experiment.js'); require('django-formset'); diff --git a/weblab/static/js/run_experiment.js b/weblab/static/js/run_experiment.js new file mode 100644 index 000000000..25bc5ca9b --- /dev/null +++ b/weblab/static/js/run_experiment.js @@ -0,0 +1,39 @@ + +var RunExperiment = function() {}; + +RunExperiment.prototype = { + init: function() { + $("#checkallbutton").click (function () { + $(".latestexperimentCheckBox").each (function () { + $(this).prop('checked', true) + }); + $(".experimentCheckBox").each (function () { + $(this).prop('checked', true) + }); + }); + $("#uncheckallbutton").click (function () { + $(".latestexperimentCheckBox").each (function () { + $(this).prop('checked', false) + }); + $(".experimentCheckBox").each (function () { + $(this).prop('checked', false) + }); + }); + $("#checklatestbutton").click (function () { + $(".latestexperimentCheckBox").each (function () { + $(this).prop('checked', true) + }); + $(".experimentCheckBox").each (function () { + $(this).prop('checked', false) + }); + }); + } +}; + + +$(document).ready(function() { + if ($("#runexperiment").length > 0) { + var page = new RunExperiment(); + page.init(); + } +}); diff --git a/weblab/templates/entities/entity_runexperiments.html b/weblab/templates/entities/entity_runexperiments.html new file mode 100644 index 000000000..d81e7bb26 --- /dev/null +++ b/weblab/templates/entities/entity_runexperiments.html @@ -0,0 +1,109 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load entities %} + +{% block title %}Experiment: run specific combinations - {% endblock title %} + +{% block body_id %}runexperiment{% endblock %} + +{% block content %} +

Run experiments using {{ entity.name }}

+ + + + +
+ {% csrf_token %} + +

+ + Create new versions of existing experiments (if unchecked, existing combinations will be skipped) +

+ + +

You may run this {{ type }} {{ preposition }} the following {{ other_type }}s.

+ +

Your {{ other_type }}s

+ + + {% for entity_object in object_list %} + + {{ entity_object.name }} + + {% endfor %} + +

Other {{ other_type }}s

+ + + {% for entity_object in other_object_list %} + + {{ entity_object.name }} + + {% endfor %} + +
+ +{% endblock %} diff --git a/weblab/templates/entities/entity_version.html b/weblab/templates/entities/entity_version.html index 274d61560..4a832d834 100644 --- a/weblab/templates/entities/entity_version.html +++ b/weblab/templates/entities/entity_version.html @@ -9,6 +9,7 @@ {% include "./includes/entity_header.html" %} {% can_create_version entity as permission %} +
@@ -38,14 +39,21 @@

{% if permission %} Add tag. -
+ Change visibility: {{ form.visibility }} - help. + help -
- {% else %} + + {% else %} Visibility: {{ visibility }} - help. + help + {% endif %} + {% if perms.experiments.create_experiment %} + Run experiments: + + Run experiments: + {% endif %}
diff --git a/weblab/templates/entities/includes/version_name.html b/weblab/templates/entities/includes/version_name.html index bef138138..6533c5c71 100644 --- a/weblab/templates/entities/includes/version_name.html +++ b/weblab/templates/entities/includes/version_name.html @@ -3,6 +3,6 @@ {% for tag in tags %} {{ tag }}{% if not forloop.last %},{% endif %} {% endfor %} -{% if tags %}({% endif %}{% spaceless %} +{% if tags %}({% endif %}{% spaceless %} {{ version.hexsha|truncatechars:11 }} {% endspaceless %}{% if tags %}){% endif %}