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/config/settings/dev.py b/weblab/config/settings/dev.py index 13b328b62..33fdab3c0 100644 --- a/weblab/config/settings/dev.py +++ b/weblab/config/settings/dev.py @@ -5,7 +5,7 @@ DEBUG = True -LOG_DEBUG = DEBUG +LOG_DEBUG = False ALLOWED_HOSTS = ['*'] diff --git a/weblab/core/combine.py b/weblab/core/combine.py index 6692a08be..47110d79d 100644 --- a/weblab/core/combine.py +++ b/weblab/core/combine.py @@ -90,9 +90,9 @@ def xml_doc(self): root, '{%s}content' % MANIFEST_NS, **{ - ('{%s}location' % MANIFEST_NS): path, - ('{%s}format' % MANIFEST_NS): fmt, - ('{%s}master' % MANIFEST_NS): 'true' if is_master else 'false' + 'location': path, + 'format': fmt, + 'master': 'true' if is_master else 'false' } ) diff --git a/weblab/datasets/templatetags/datasets.py b/weblab/datasets/templatetags/datasets.py index c1671fe0c..a0820dcb6 100644 --- a/weblab/datasets/templatetags/datasets.py +++ b/weblab/datasets/templatetags/datasets.py @@ -8,7 +8,7 @@ @register.simple_tag(takes_context=True) def can_create_dataset(context): user = context['user'] - return user.has_perm('datasets.create_dataset_experiment') + return user.has_perm('datasets.create_dataset') @register.filter diff --git a/weblab/entities/forms.py b/weblab/entities/forms.py index 3b4fe1cb6..7ab7b238d 100644 --- a/weblab/entities/forms.py +++ b/weblab/entities/forms.py @@ -41,7 +41,12 @@ class Meta: class ProtocolEntityForm(EntityForm): class Meta: model = ProtocolEntity - fields = ['name'] + fields = ['name', 'is_fitting_spec'] + + is_fitting_spec = forms.BooleanField( + label='This protocol is a parameter fitting specification', + widget=forms.CheckboxInput(attrs={'class': 'inline'}), + required=False) class EntityVersionForm(forms.Form): @@ -57,15 +62,14 @@ class EntityVersionForm(forms.Form): label='Description of this version', widget=forms.Textarea) rerun_expts = forms.BooleanField( - label='', - help_text='Re-run experiments involving the previous version of this %s', + label='Re-run experiments involving the previous version of this %s', widget=forms.CheckboxInput(attrs={'class': 'inline'}), required=False) def __init__(self, *args, **kwargs): entity_type = kwargs.pop('entity_type') super().__init__(*args, **kwargs) - self.fields['rerun_expts'].help_text = self.fields['rerun_expts'].help_text % entity_type + self.fields['rerun_expts'].label = self.fields['rerun_expts'].label % entity_type class EntityChangeVisibilityForm(UserKwargModelFormMixin, forms.Form): diff --git a/weblab/entities/migrations/0014_entity_is_fitting_spec.py b/weblab/entities/migrations/0014_entity_is_fitting_spec.py new file mode 100644 index 000000000..c72687bd0 --- /dev/null +++ b/weblab/entities/migrations/0014_entity_is_fitting_spec.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.15 on 2019-05-30 13:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def set_initial_fitting_specs(apps, schema_editor): + Entity = apps.get_model('entities', 'Entity') + Entity.objects.filter(name__startswith='Fit ').update(is_fitting_spec=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('entities', '0013_auto_20190408_1354'), + ] + + operations = [ + migrations.AddField( + model_name='entity', + name='is_fitting_spec', + field=models.BooleanField( + default=False, + help_text='This protocol is a parameter fitting specification'), + ), + migrations.RunPython(set_initial_fitting_specs, migrations.RunPython.noop), + ] diff --git a/weblab/entities/models.py b/weblab/entities/models.py index cf6e907b4..c6f4f85fd 100644 --- a/weblab/entities/models.py +++ b/weblab/entities/models.py @@ -37,6 +37,11 @@ class Entity(UserCreatedModelMixin, models.Model): name = models.CharField(validators=[MinLengthValidator(2)], max_length=255) + is_fitting_spec = models.BooleanField( + default=False, + help_text="This protocol is a parameter fitting specification", + ) + class Meta: ordering = ['name'] unique_together = ('entity_type', 'name', 'author') 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..f07d3e888 100644 --- a/weblab/entities/tests/test_views.py +++ b/weblab/entities/tests/test_views.py @@ -319,7 +319,7 @@ def test_version_json(self, client, logged_in_user, helpers, can_create_expt): assert ver['name'] == 'mymodel' assert ver['id'] == version.hexsha - assert ver['author'] == 'model author' + assert ver['author'] == 'author' # Commit author not entity author assert ver['entityId'] == model.pk assert ver['visibility'] == 'public' assert ( @@ -2055,3 +2055,420 @@ 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 + + # 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 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..727687a5b 100644 --- a/weblab/entities/views.py +++ b/weblab/entities/views.py @@ -24,7 +24,7 @@ JsonResponse, ) from django.core.exceptions import PermissionDenied -from django.db.models import F, Q +from django.db.models import Count, F, Q from django.utils.decorators import method_decorator from django.utils.text import get_valid_filename from django.views import View @@ -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 @@ -230,7 +230,7 @@ def get(self, request, *args, **kwargs): return JsonResponse({ 'version': { 'id': commit.hexsha, - 'author': obj.author.full_name, + 'author': commit.author.name, 'entityId': obj.id, 'visibility': obj.get_version_visibility(commit.hexsha), 'created': commit.committed_at, @@ -268,7 +268,11 @@ def get_context_data(self, **kwargs): experiments = Experiment.objects.filter(**{ entity_type: entity.pk, ('%s_version' % entity_type): commit.hexsha, - }).select_related(other_type).order_by(other_type, '-created_at') + }).annotate( + version_count=Count('versions'), + ).filter( + version_count__gt=0, + ).select_related(other_type).order_by(other_type, '-created_at') experiments = [ exp for exp in experiments @@ -1016,3 +1020,77 @@ 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 + + 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 + return HttpResponseRedirect( + reverse('entities:version', args=[kwargs['entity_type'], kwargs['pk'], 'latest'])) diff --git a/weblab/experiments/models.py b/weblab/experiments/models.py index 58f077c47..2574aa6c6 100644 --- a/weblab/experiments/models.py +++ b/weblab/experiments/models.py @@ -82,9 +82,9 @@ def viewers(self): :return: `set` of `User` objects """ - if self.protocol.visibility == Visibility.PUBLIC: + if self.protocol.visibility != Visibility.PRIVATE: return self.model.viewers - elif self.model.visibility == Visibility.PUBLIC: + elif self.model.visibility != Visibility.PRIVATE: return self.protocol.viewers else: return self.model.viewers & self.protocol.viewers diff --git a/weblab/experiments/processing.py b/weblab/experiments/processing.py index 674614ae7..5da4cd3fe 100644 --- a/weblab/experiments/processing.py +++ b/weblab/experiments/processing.py @@ -6,6 +6,7 @@ from django.conf import settings from django.core.exceptions import MultipleObjectsReturned from django.core.urlresolvers import reverse +from django.utils.timezone import now from .emails import send_experiment_finished_email from .models import Experiment, ExperimentVersion, RunningExperiment @@ -94,6 +95,8 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re 'password': settings.CHASTE_PASSWORD, 'isAdmin': user.is_staff, } + if protocol.is_fitting_spec: + body['dataset'] = body['fittingSpec'] = body['protocol'] try: response = requests.post(settings.CHASTE_URL, body) @@ -179,6 +182,8 @@ def process_callback(data, files): exp.return_text = data.get('returnmsg') or 'finished' if exp.is_running: exp.return_text = 'running' + if exp.is_finished: + exp.finished_at = now() exp.save() diff --git a/weblab/experiments/tests/test_processing.py b/weblab/experiments/tests/test_processing.py index bcb1b2bdc..dae367217 100644 --- a/weblab/experiments/tests/test_processing.py +++ b/weblab/experiments/tests/test_processing.py @@ -1,4 +1,5 @@ import uuid +from datetime import timedelta from pathlib import Path from unittest.mock import Mock, patch @@ -6,6 +7,7 @@ from django.conf import settings from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils.timezone import now from core import recipes from experiments.models import Experiment, ExperimentVersion, RunningExperiment @@ -250,6 +252,8 @@ def test_returns_error_if_invalid_returntype(self, queued_experiment): ]) def test_records_finished_status(self, returned_status, stored_status, archive_upload, queued_experiment): + assert not queued_experiment.is_finished + assert not queued_experiment.finished_at result = process_callback({ 'signature': queued_experiment.signature, 'returntype': returned_status, @@ -262,6 +266,9 @@ def test_records_finished_status(self, returned_status, stored_status, queued_experiment.refresh_from_db() assert queued_experiment.status == stored_status assert queued_experiment.return_text == 'finished' + assert queued_experiment.is_finished + assert queued_experiment.finished_at > queued_experiment.created_at + assert now() - queued_experiment.finished_at < timedelta(0, 10, 0) # 10 secs @pytest.mark.parametrize('returned_status', [ 'success', diff --git a/weblab/experiments/views.py b/weblab/experiments/views.py index 62fd88a37..415b0266c 100644 --- a/weblab/experiments/views.py +++ b/weblab/experiments/views.py @@ -8,6 +8,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin from django.core.urlresolvers import reverse from django.db.models import F, OuterRef, Q, Subquery +from django.db.models.functions import Coalesce from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator @@ -44,9 +45,9 @@ class ExperimentMatrixJsonView(View): Serve up JSON for experiment matrix """ @classmethod - def entity_json(cls, entity, version, *, extend_name, visibility, author): + def entity_json(cls, entity, version, *, extend_name, visibility, author, friendly_version=''): if extend_name: - name = '%s @ %s' % (entity.name, version) + name = '%s @ %s' % (entity.name, friendly_version or version) else: name = entity.name @@ -115,6 +116,7 @@ def versions_query(self, entity_type, requested_versions, entity_query, visibili 'entity__entity', ).annotate( author_name=F('entity__entity__author__full_name'), + friendly_name=Coalesce(F('tags__tag'), F('sha')), ) return q_entity_versions @@ -126,6 +128,7 @@ def get(self, request, *args, **kwargs): model_versions = request.GET.getlist('modelVersions[]') protocol_versions = request.GET.getlist('protoVersions[]') subset = request.GET.get('subset', 'all' if model_pks or protocol_pks else 'moderated') + show_fits = 'show_fits' in request.GET if model_versions and len(model_pks) > 1: return JsonResponse({ @@ -202,7 +205,10 @@ def get(self, request, *args, **kwargs): protocol_visibility_where = ~Q(visibility='private') | Q(entity__entity__in=visible_entities) protocol_ids = set(protocol_pks) else: - protocol_visibility_where = visibility_where + if show_fits: + protocol_visibility_where = visibility_where & Q(entity__entity__is_fitting_spec=True) + else: + protocol_visibility_where = visibility_where & Q(entity__entity__is_fitting_spec=False) protocol_ids = entity_ids q_protocols = ProtocolEntity.objects.filter(id__in=protocol_ids) @@ -212,14 +218,16 @@ def get(self, request, *args, **kwargs): model_versions = [self.entity_json(version.entity.entity, version.sha, extend_name=bool(model_versions), visibility=version.visibility, - author=version.author_name) + author=version.author_name, + friendly_version=version.friendly_name) for version in q_model_versions] model_versions = {ver['id']: ver for ver in model_versions} protocol_versions = [self.entity_json(version.entity.entity, version.sha, extend_name=bool(protocol_versions), visibility=version.visibility, - author=version.author_name) + author=version.author_name, + friendly_version=version.friendly_name) for version in q_protocol_versions] protocol_versions = {ver['id']: ver for ver in protocol_versions} diff --git a/weblab/static/js/db.js b/weblab/static/js/db.js index 34056a6a1..10c5a1f92 100644 --- a/weblab/static/js/db.js +++ b/weblab/static/js/db.js @@ -436,7 +436,8 @@ function parseLocation () { var base = $('#matrixdiv').data('base-href'), rest = "", - ret = {}; + ret = {}, + queryParams = (new URL(document.location)).searchParams; if (document.location.pathname.substr(0, base.length) == base) { @@ -445,6 +446,14 @@ function parseLocation () $('.showButton').removeClass("selected"); $('.showMyButton').hide(); + if (queryParams.has('show_fits')) { + ret.show_fits = true; + $('#showFittingExpts').addClass("selected"); + $('#showPredictionExpts').removeClass("selected"); + } else { + $('#showPredictionExpts').addClass("selected"); + $('#showFittingExpts').removeClass("selected"); + } if (rest.length > 0) { var items = rest.split("/"), @@ -622,47 +631,79 @@ function prepareMatrix () $("#comparisonMatrix").hide(); function getBaseUrl() { - var url = $(div).data('base-href'); + var url = $(div).data('base-href'), + suffix = $('.showButton.selected').data('suffix'); if (url.substr(-1) === '/') { url = url.slice(0, -1); } - return url; + if (suffix) { + url = url + '/' + suffix; + } + return url + '?'; + } + + function getFittingParams() { + params = {}; + if ($('#showFittingExpts').hasClass("selected")) params['show_fits'] = true; + return params; } function hideModeratedParams(hideModels, hideProtocols) { - var params = {}; + var params = getFittingParams(); if (hideModels) params['moderated-models'] = false; if (hideProtocols) params['moderated-protocols'] = false; - return params + return params; } // The my/public/moderated view buttons $("#showModeratedExpts").click(function () { if (!$(this).hasClass("selected")) - document.location.href = getBaseUrl(); + { + $('.showButton').removeClass("selected"); + $(this).addClass("selected"); + document.location.href = getBaseUrl() + $.param(getFittingParams()); + } }); $("#showPublicExpts").click(function () { if (!$(this).hasClass("selected")) - document.location.href = getBaseUrl() + '/public'; + { + $('.showButton').removeClass("selected"); + $(this).addClass("selected"); + document.location.href = getBaseUrl() + $.param(getFittingParams()); + } }); $("#showAllExpts").click(function () { if (!$(this).hasClass("selected")) - document.location.href = getBaseUrl() + '/all'; + { + $('.showButton').removeClass("selected"); + $(this).addClass("selected"); + document.location.href = getBaseUrl() + $.param(getFittingParams()); + } }); $("#showMyExpts").click(function () { if (!$(this).hasClass("selected")) - document.location.href = getBaseUrl() + '/mine'; + { + $('.showButton').removeClass("selected"); + $(this).addClass("selected"); + document.location.href = getBaseUrl() + $.param(getFittingParams()); + } }); $("#showMyExptsModels").click(function () { var hideModels = hiddenToggle($(this)), hideProtocols = $("#showMyExptsProtocols").text().substr(0,4) == 'Show'; - document.location.href = getBaseUrl() + '/mine?' + $.param(hideModeratedParams(hideModels, hideProtocols)); + document.location.href = getBaseUrl() + $.param(hideModeratedParams(hideModels, hideProtocols)); }); $("#showMyExptsProtocols").click(function () { var hideProtocols = hiddenToggle($(this)), hideModels = $("#showMyExptsModels").text().substr(0,4) == 'Show'; - document.location.href = getBaseUrl() + '/mine?' + $.param(hideModeratedParams(hideModels, hideProtocols)); + document.location.href = getBaseUrl() + $.param(hideModeratedParams(hideModels, hideProtocols)); }); + + // Hacky fitting experiment support + $('.showFitsButton').click(function() { + $('.showFitsButton').toggleClass("selected"); + document.location.href = getBaseUrl() + $.param(getFittingParams()); + }); } /** diff --git a/weblab/static/js/experiment.js b/weblab/static/js/experiment.js index a18fd4994..32a3f1f9c 100644 --- a/weblab/static/js/experiment.js +++ b/weblab/static/js/experiment.js @@ -26,6 +26,43 @@ function init() { var visualizers = {}; + // Handle selecting a linked dataset to overlay + function load_linked_dataset(dataset_json_url) { + if (dataset_json_url) { + $.getJSON(dataset_json_url, function(json) { + dataset_json = json.version; + $('#dataset-link').data({json: dataset_json, file: null}); + for (var i=0; i"; @@ -282,7 +319,7 @@ function init() { //console.log(v); var dv = doc.version; - dv.name.text("Version: " + v.name + " "); + dv.name.html("Version: " + v.name + " "); // If an experiment, show indication of status, perhaps including a note that we don't expect any results yet! //if (entityType == 'experiment') @@ -698,6 +735,8 @@ function init() { $(df.display).empty(); df.display.appendChild (f.div[pluginName]); f.viz[pluginName].show (); + // Let dataset link know we've initialised a plot + $('#dataset-link').data("viz", f.viz[pluginName]); // Show parent div of the file display, and scroll there doc.version.filedetails.style.display = "block"; @@ -706,6 +745,7 @@ function init() { } else { doc.version.filedetails.style.display = "none"; + $('#dataset-link').data("viz", null); } } @@ -805,7 +845,6 @@ function init() { filedetails : document.getElementById("entityversionfiledetails"), experimentlist: document.getElementById("entityexperimentlist"), experimentpartners: document.getElementById("entityexperimentlistpartners"), - switcher: document.getElementById("experiment-files-switcher"), visibility: document.getElementById("versionVisibility"), visibilityAction : document.getElementById("versionVisibilityAction"), deleteBtn: document.getElementById("deleteVersion"), diff --git a/weblab/static/js/expt_common.js b/weblab/static/js/expt_common.js index 0a005cdf0..7fcf6db2f 100644 --- a/weblab/static/js/expt_common.js +++ b/weblab/static/js/expt_common.js @@ -290,6 +290,7 @@ function getCSV (file) /** * Extract key data for a file if available. + * If not, generates default labels 'line 1' etc. * * @param file the file to get key data for * @param numTraces the number of values to expect in a key vector @@ -301,8 +302,15 @@ function getKeyValues(file, numTraces) { var keyData = getCSVColumns(file.keyFile); if (keyData.length > 0) + { for (var i=0; i 0) { + var page = new RunExperiment(); + page.init(); + } +}); diff --git a/weblab/static/js/visualizers/displayPlotFlot/displayPlotFlot.js b/weblab/static/js/visualizers/displayPlotFlot/displayPlotFlot.js index 132a71cd6..b1961502a 100644 --- a/weblab/static/js/visualizers/displayPlotFlot/displayPlotFlot.js +++ b/weblab/static/js/visualizers/displayPlotFlot/displayPlotFlot.js @@ -3,7 +3,7 @@ var common = require('../../expt_common.js'); var choicesDivId = 'choices', resetButtonDivId = 'flot-buttons-div', - colouredSpanIdPrefix = 'span', + colouredSpanIdPrefix = 'legend-colour-span-', legendDivId = 'legend', tooltipId = 'flotTooltip', plottedGraph = {}, // TODO: probably safer if this is an instance property! @@ -388,27 +388,41 @@ contentFlotPlot.prototype.getContentsCallback = function (succ) thisFile.keyFile.getContents (this); return; } + this.setUp = true; + this.drawPlot(); + } +}; +contentFlotPlot.prototype.drawPlot = function () +{ + var thisFile = this.file, thisFileId = thisFile.id, thisDiv = this.div; + $(thisDiv).empty(); var styleLinespointsOrPoints = isStyleLinespointsOrPoints(thisFile.linestyle); var csvData = styleLinespointsOrPoints ? common.getCSVColumnsNonDownsampled (thisFile) : common.getCSVColumnsDownsampled (thisFile); var keyVals = common.getKeyValues(thisFile, csvData.length); + var data_file = $('#dataset-link').data('file'); + if (data_file) + { + // Overlay expt'l data + var data_cols = styleLinespointsOrPoints ? common.getCSVColumnsNonDownsampled(data_file) : + common.getCSVColumnsDownsampled(data_file), + data_key = common.getKeyValues(data_file, data_cols.length); + data_cols.shift(); // Remove t + data_key.shift(); + csvData = csvData.concat(data_cols); + keyVals = keyVals.concat(data_key); + console.log(keyVals); + } + var datasets = {}; for (var i = 1; i < csvData.length; i++) { - var curData = [], label = "line " + i; + var curData = []; for (var j = 0; j < csvData[i].length; j++) curData.push ([csvData[i][j].x, csvData[i][j].y]); - - if (keyVals.length == csvData.length) - { - if (thisFile.keyName) - label = thisFile.keyName + " = " + keyVals[i] + " " + thisFile.keyUnits; - else - label = keyVals[i]; - } - datasets["line" + i] = {label: label, data: curData}; + datasets["line" + i] = {label: keyVals[i], data: curData}; } // Some of the plots won't come from specified plots, so these are missing. @@ -474,7 +488,6 @@ contentFlotPlot.prototype.getContentsCallback = function (succ) // Save data for export if user requests it common.allowPlotExport(thisFile.name, transformForExport(datasets), {'x': x_label, 'y': y_label}); - } }; contentFlotPlot.prototype.show = function () @@ -485,6 +498,12 @@ contentFlotPlot.prototype.show = function () this.file.getContents (this); }; +contentFlotPlot.prototype.redraw = function () +{ + if (this.setUp) + this.drawPlot(); +}; + function contentFlotPlotComparer (file, div) { this.file = file; @@ -616,10 +635,8 @@ contentFlotPlotComparer.prototype.showContents = function () var plotLabelStripText = $.data(document.body, 'plotLabelStripText'); if (plotLabelStripText) label = label.replace(plotLabelStripText, ""); - if (keyVals.length == csvData.length) - label += ", " + eachCSVData.file.keyName + " = " + keyVals[i] + " " + eachCSVData.file.keyUnits; - else if (csvData.length > 2) - label += " line " + i; + if (csvData.length > 2 || keyVals[i].substr(0, 5) !== "line ") + label += ", " + keyVals[i]; datasets[key] = {label: label, data: curData, color: curColor}; var colouredSpan = $('').attr('id', colouredSpanIdPrefix + curColor) @@ -675,6 +692,12 @@ contentFlotPlotComparer.prototype.show = function () } }; +contentFlotPlotComparer.prototype.redraw = function () +{ + if (this.setUp) + this.showContents(); +}; + function flotContent () { diff --git a/weblab/static/js/visualizers/displayPlotHC/displayPlotHC.js b/weblab/static/js/visualizers/displayPlotHC/displayPlotHC.js index 4f6816420..2c75e62e8 100644 --- a/weblab/static/js/visualizers/displayPlotHC/displayPlotHC.js +++ b/weblab/static/js/visualizers/displayPlotHC/displayPlotHC.js @@ -88,10 +88,33 @@ HCPlotter.prototype.getContentsCallback = function (succ) thisFile.keyFile.getContents (this); return; } + this.setUp = true; + this.drawPlot(); + } +}; +HCPlotter.prototype.drawPlot = function () +{ + var thisFile = this.file; + $(this.div).empty(); var csvData = (thisFile.linestyle == "linespoints" || thisFile.linestyle == "points") ? common.getCSVColumnsNonDownsampled (thisFile) : common.getCSVColumnsDownsampled (thisFile); var keyVals = common.getKeyValues(thisFile, csvData.length); - + + var data_file = $('#dataset-link').data('file'); + if (data_file) + { + // Overlay expt'l data + var data_cols = (thisFile.linestyle == "linespoints" || thisFile.linestyle == "points") ? + common.getCSVColumnsNonDownsampled(data_file) : + common.getCSVColumnsDownsampled(data_file), + data_key = common.getKeyValues(data_file, data_cols.length); + data_cols.shift(); // Remove t + data_key.shift(); + csvData = csvData.concat(data_cols); + keyVals = keyVals.concat(data_key); + console.log(keyVals); + } + var div = document.createElement("div"); var id = "hcplot-" + thisFile.id.replace(/\W/g, ''); div.id = id; @@ -105,22 +128,10 @@ HCPlotter.prototype.getContentsCallback = function (succ) var curData = []; for (var j = 0; j < csvData[i].length; j++) curData.push ([csvData[i][j].x, csvData[i][j].y]); - var label; - if (keyVals.length == csvData.length) - { - if (thisFile.keyName) - label = thisFile.keyName + " = " + keyVals[i] + " " + thisFile.keyUnits; - else - label = keyVals[i]; - } - else - label = "line " + i; - datasets.push ({name : label, data: curData}); + datasets.push ({name: keyVals[i], data: curData}); } doHcPlot(id, datasets, thisFile); - } - }; HCPlotter.prototype.show = function () @@ -129,6 +140,12 @@ HCPlotter.prototype.show = function () this.file.getContents (this); }; +HCPlotter.prototype.redraw = function () +{ + if (this.setUp) + this.drawPlot(); +}; + function HCPlotterComparer (file, div) { this.file = file; @@ -254,12 +271,10 @@ HCPlotterComparer.prototype.showContents = function () var plotLabelStripText = $.data(document.body, 'plotLabelStripText'); if (plotLabelStripText) label = label.replace(plotLabelStripText, ""); - if (keyVals.length == csvData.length) - label += ", " + csvFile.keyName + " = " + keyVals[i] + " " + csvFile.keyUnits - else if (csvData.length > 2) - label += " line " + i; + if (csvData.length > 2 || keyVals[i].substr(0, 5) !== "line ") + label += ", " + keyVals[i]; - datasets.push ({name : label, data: curData}); + datasets.push({name: label, data: curData}); } } @@ -278,6 +293,12 @@ HCPlotterComparer.prototype.show = function () this.showContents (); }; +HCPlotterComparer.prototype.redraw = function () +{ + if (this.setUp) + this.showContents(); +}; + function HCPlot () { this.name = "displayPlotHC"; diff --git a/weblab/static/sass/style.scss b/weblab/static/sass/style.scss index 2356c4a8d..7dceefe79 100644 --- a/weblab/static/sass/style.scss +++ b/weblab/static/sass/style.scss @@ -243,13 +243,17 @@ p.right form { label { - display: block; + display: inline; } input { + display: block; &[type=text], &[type=email], &[type=password] { width: 20em; } } + textarea { + display: block; + } small, .helptext, p + ul, td > ul { color: #aaa; font-size: small; diff --git a/weblab/templates/datasets/dataset_list.html b/weblab/templates/datasets/dataset_list.html index ac725bf46..d846db8e9 100644 --- a/weblab/templates/datasets/dataset_list.html +++ b/weblab/templates/datasets/dataset_list.html @@ -12,7 +12,7 @@

Your datasets

{% if permission %} Create a new dataset {% else %} - Your account doesn't have the authority to upload {{ type }}s; please contact us to request permission. + Your account doesn't have the authority to upload datasets; please contact us to request permission. {% endif %}
    diff --git a/weblab/templates/entities/entity_list.html b/weblab/templates/entities/entity_list.html index 59425908c..abecfea0d 100644 --- a/weblab/templates/entities/entity_list.html +++ b/weblab/templates/entities/entity_list.html @@ -7,7 +7,7 @@ models protocols - experiments + experiments

    Your {{ type }}s

    diff --git a/weblab/templates/entities/entity_runexperiments.html b/weblab/templates/entities/entity_runexperiments.html new file mode 100644 index 000000000..dbca6064e --- /dev/null +++ b/weblab/templates/entities/entity_runexperiments.html @@ -0,0 +1,116 @@ +{% 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/entity_header.html b/weblab/templates/entities/includes/entity_header.html index 7487c4cd2..610af2dad 100644 --- a/weblab/templates/entities/includes/entity_header.html +++ b/weblab/templates/entities/includes/entity_header.html @@ -2,7 +2,7 @@ {% load entities %}

    - {{ type|capfirst }}: {{ entity.name }} + {% if entity.is_fitting_spec %}Parameter fitting specification{% else %}{{ type|capfirst }}{% endif %}: {{ entity.name }}

    diff --git a/weblab/templates/experiments/experiments.html b/weblab/templates/experiments/experiments.html index dc46ced51..ce4681a97 100644 --- a/weblab/templates/experiments/experiments.html +++ b/weblab/templates/experiments/experiments.html @@ -9,15 +9,18 @@

    Available experiments

    Show: - - + + {% if user.is_authenticated %} - - + +
    {% endif %} +
    + +

    This matrix shows the latest versions (visible to you) of the models and protocols in our database, with the corresponding experiments. diff --git a/weblab/templates/experiments/experimentversion_detail.html b/weblab/templates/experiments/experimentversion_detail.html index f1150019d..111d70aec 100644 --- a/weblab/templates/experiments/experimentversion_detail.html +++ b/weblab/templates/experiments/experimentversion_detail.html @@ -27,6 +27,7 @@

    Created by {{ version.author.full_name }}. + {% if version.finished_at %}Took {{ version.created_at|timesince:version.finished_at }}.{% endif %} Visibility: {{ version.visibility }} help. @@ -68,12 +69,17 @@

    -
    - - -
    + {% if experiment.protocol.protocol_experimental_datasets.exists %} + - + {% endif %}
    diff --git a/weblab/templates/home.html b/weblab/templates/home.html index e33c0febe..3af2b77df 100644 --- a/weblab/templates/home.html +++ b/weblab/templates/home.html @@ -20,7 +20,7 @@

    Quick start links

    an IV curve of the fast sodium current, or S1-S2 and steady state restitution; or set up your own comparisons. -
  • View prototype parameter fitting results.
  • +
  • View prototype parameter fitting results.
  • {% if user.is_authenticated %} {% url 'entities:list' 'model' as analyse_link %} {% else %}