diff --git a/weblab/datasets/tests/test_views.py b/weblab/datasets/tests/test_views.py index 93b35818b..fb9b1050d 100644 --- a/weblab/datasets/tests/test_views.py +++ b/weblab/datasets/tests/test_views.py @@ -637,3 +637,134 @@ def test_nonexistent_dataset_redirects_anonymous_to_login(self, client, helpers, def test_nonexistent_dataset_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 TestDatasetCompareFittingResultsView: + def test_shows_fittings_related_to_dataset(self, client, fittingresult_version): + fit = fittingresult_version.fittingresult + + # should not be included, as it uses a different dataset + recipes.fittingresult_version.make() + + response = client.get('/datasets/%d/fittings' % fit.dataset.pk) + + assert response.status_code == 200 + assert response.context['comparisons'] == [(fit.model, [fit])] + + def test_groups_by_model(self, client, helpers, public_dataset): + m1, m2 = recipes.model.make(_quantity=2) + m1v = helpers.add_cached_version(m1, visibility='public') + m2v = helpers.add_cached_version(m2, visibility='public') + + # Create publicly visible fitting result versions + fit1_m1 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model=m1, + fittingresult__model_version=m1v, + fittingresult__fittingspec_version__visibility='public', + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit2_m1 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model=m1, + fittingresult__model_version=m1v, + fittingresult__fittingspec_version__visibility='public', + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit3_m2 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model=m2, + fittingresult__model_version=m2v, + fittingresult__fittingspec_version__visibility='public', + fittingresult__protocol_version__visibility='public', + ).fittingresult + + response = client.get('/datasets/%d/fittings' % public_dataset.id) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (m1, [fit2_m1, fit1_m1]), + (m2, [fit3_m2]), + ] + + def test_multiple_model_versions_for_dataset(self, client, helpers, public_dataset): + m1, m2 = recipes.model.make(_quantity=2) + m1v1 = helpers.add_cached_version(m1, visibility='public') + m1v2 = helpers.add_cached_version(m1, visibility='public') + m2v = helpers.add_cached_version(m2, visibility='public') + + # Create publicly visible fitting result versions + fit1_m1v1 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model=m1, + fittingresult__model_version=m1v1, + fittingresult__fittingspec_version__visibility='public', + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit2_m1v2 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model=m1, + fittingresult__model_version=m1v2, + fittingresult__fittingspec_version__visibility='public', + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit3_m2v = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model=m2, + fittingresult__model_version=m2v, + fittingresult__fittingspec_version__visibility='public', + fittingresult__protocol_version__visibility='public', + ).fittingresult + + response = client.get( + '/datasets/%d/fittings' % public_dataset.id + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (m1, [fit2_m1v2, fit1_m1v1]), + (m2, [fit3_m2v]), + ] + + def test_ensure_private_results_are_not_shown(self, client, public_dataset): + recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model_version__visibility='private', + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + ) + + recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model_version__visibility='public', + fittingresult__protocol_version__visibility='private', + fittingresult__fittingspec_version__visibility='public', + ) + + recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model_version__visibility='public', + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='private', + ) + + fit = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__model_version__visibility='public', + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + response = client.get( + '/datasets/%d/fittings' % public_dataset.id + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (fit.model, [fit]), + ] diff --git a/weblab/datasets/urls.py b/weblab/datasets/urls.py index b293d1b1f..733f01f0c 100644 --- a/weblab/datasets/urls.py +++ b/weblab/datasets/urls.py @@ -74,4 +74,10 @@ views.DatasetRenameView.as_view(), name='rename', ), + + url( + r'^(?P\d+)/fittings$', + views.DatasetCompareFittingResultsView.as_view(), + name='compare_fittings', + ), ] diff --git a/weblab/datasets/views.py b/weblab/datasets/views.py index 682a2a6ab..53a4cae9e 100644 --- a/weblab/datasets/views.py +++ b/weblab/datasets/views.py @@ -1,6 +1,7 @@ import mimetypes import os.path import shutil +from itertools import groupby from zipfile import ZipFile from braces.views import UserFormKwargsMixin @@ -25,6 +26,7 @@ from accounts.forms import OwnershipTransferForm from core.combine import ManifestWriter from core.visibility import VisibilityMixin +from fitting.models import FittingResult from .forms import ( DatasetAddFilesForm, @@ -332,3 +334,39 @@ def post(self, request, *args, **kwargs): def get_success_url(self, *args, **kwargs): ns = self.request.resolver_match.namespace return reverse(ns + ':detail', args=[self._get_object().id]) + + +class DatasetCompareFittingResultsView(DetailView): + """ + List fitting results for this dataset, with selection boxes for comparison + """ + model = Dataset + template_name = 'datasets/compare_fittings.html' + + def _get_object(self): + if not hasattr(self, 'object'): + self.object = self.get_object() + return self.object + + def get_context_data(self, **kwargs): + dataset = self._get_object() + + fittings = FittingResult.objects.filter( + dataset=dataset.pk, + ).select_related( + 'model', + ).order_by('model', '-model_version__timestamp', '-protocol_version__timestamp') + + # Ensure all are visible to user + fittings = [ + fit for fit in fittings + if fit.is_visible_to_user(self.request.user) + ] + + # Group fittings by model + kwargs['comparisons'] = [ + (obj, list(fits)) + for (obj, fits) in groupby(fittings, lambda fit: fit.model) + ] + + return super().get_context_data(**kwargs) diff --git a/weblab/entities/templatetags/entities.py b/weblab/entities/templatetags/entities.py index e42cc8a15..17e7d0a4b 100644 --- a/weblab/entities/templatetags/entities.py +++ b/weblab/entities/templatetags/entities.py @@ -123,6 +123,11 @@ def name_of_protocol(experiment): return '%s @ %s' % (experiment.protocol.name, experiment.protocol_version.get_name()) +@register.filter +def name_of_fittingspec(experiment): + return '%s @ %s' % (experiment.fittingspec.name, experiment.fittingspec_version.get_name()) + + def _url_friendly_label(entity, version): """ Get URL-friendly version label for a commit diff --git a/weblab/entities/tests/test_views.py b/weblab/entities/tests/test_views.py index fbb97ce27..c6a8b8aaa 100644 --- a/weblab/entities/tests/test_views.py +++ b/weblab/entities/tests/test_views.py @@ -540,6 +540,12 @@ def test_complex_visibilities(self, client, logged_in_user, other_user, helpers) @pytest.mark.django_db class TestModelEntityCompareExperimentsView: + def test_has_visibility_form(self, client, helpers, public_model): + response = client.get('/entities/models/%d/versions/%s/compare' % + (public_model.pk, public_model.repocache.latest_version.sha)) + assert 'form' in response.context + assert response.context['form'].initial.get('visibility') == 'public' + def test_shows_related_experiments(self, client, helpers, experiment_version): exp = experiment_version.experiment model_version = exp.model.repocache.latest_version @@ -652,6 +658,427 @@ def test_returns_404_if_commit_not_found(self, client, logged_in_user): assert response.status_code == 404 +@pytest.mark.django_db +class TestEntityCompareFittingResultsView: + def test_has_visibility_form(self, client, helpers, public_model): + response = client.get('/entities/models/%d/versions/%s/fittings' % + (public_model.pk, public_model.repocache.latest_version.sha)) + assert 'form' in response.context + assert response.context['form'].initial.get('visibility') == 'public' + + def test_shows_fittings_related_to_model_version(self, client, fittingresult_version): + fit = fittingresult_version.fittingresult + + # should not be included, as it uses a different model and version + recipes.fittingresult_version.make() + + # should not be included, as it uses a different version of this model + recipes.fittingresult_version.make(fittingresult__model=fit.model) + + response = client.get( + '/entities/models/%d/versions/%s/fittings' % (fit.model.pk, fit.model_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [(fit.dataset, [fit])] + + def test_groups_by_dataset_for_model(self, client, public_model): + model_version = public_model.repocache.latest_version + dataset1, dataset2 = recipes.dataset.make(_quantity=2, visibility='public') + + # Create publicly visible fitting result versions + ds1fit1 = recipes.fittingresult_version.make( + fittingresult__dataset=dataset1, + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + ds1fit2 = recipes.fittingresult_version.make( + fittingresult__dataset=dataset1, + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + ds2fit1 = recipes.fittingresult_version.make( + fittingresult__dataset=dataset2, + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + response = client.get( + '/entities/models/%d/versions/%s/fittings' % (public_model.id, model_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (dataset1, [ds1fit2, ds1fit1]), + (dataset2, [ds2fit1]), + ] + + def test_ensure_private_results_are_not_shown_for_model_version(self, client, public_model): + model_version = public_model.repocache.latest_version + recipes.fittingresult_version.make( + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='private', + fittingresult__fittingspec_version__visibility='public', + fittingresult__dataset__visibility='public' + ) + + recipes.fittingresult_version.make( + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='private', + fittingresult__dataset__visibility='public' + ) + + recipes.fittingresult_version.make( + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + fittingresult__dataset__visibility='private' + ) + + fit = recipes.fittingresult_version.make( + fittingresult__model=public_model, + fittingresult__model_version=model_version, + fittingresult__protocol_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + fittingresult__dataset__visibility='public' + ).fittingresult + + response = client.get( + '/entities/models/%d/versions/%s/fittings' % (public_model.id, model_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (fit.dataset, [fit]), + ] + + def test_shows_fittings_related_to_protocol_version(self, client, fittingresult_version): + fit = fittingresult_version.fittingresult + + # should not be included, as it uses a different protocol + recipes.fittingresult_version.make() + + # should not be included, as it uses a different version of this protocol + recipes.fittingresult_version.make(fittingresult__protocol=fit.protocol) + + response = client.get( + '/entities/protocols/%d/versions/%s/fittings' % (fit.protocol.pk, fit.protocol_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [(fit.dataset, [(fit.model, [fit])])] + + def test_groups_by_dataset_for_protocol(self, client, helpers, public_protocol): + protocol_version = public_protocol.repocache.latest_version + ds1, ds2 = recipes.dataset.make(_quantity=2, visibility='public') + m1, m2 = recipes.model.make(_quantity=2) + m1v = helpers.add_cached_version(m1, visibility='public') + m2v = helpers.add_cached_version(m2, visibility='public') + + # Create publicly visible fitting result versions + fit1_ds1_m1 = recipes.fittingresult_version.make( + fittingresult__dataset=ds1, + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model=m1, + fittingresult__model_version=m1v, + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + fit2_ds1_m2 = recipes.fittingresult_version.make( + fittingresult__dataset=ds1, + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model=m2, + fittingresult__model_version=m2v, + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + fit3_ds2_m1 = recipes.fittingresult_version.make( + fittingresult__dataset=ds2, + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model=m1, + fittingresult__model_version=m1v, + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + response = client.get( + '/entities/protocols/%d/versions/%s/fittings' % (public_protocol.id, protocol_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (ds1, [ + (m1, [fit1_ds1_m1]), + (m2, [fit2_ds1_m2]), + ]), + (ds2, [ + (m1, [fit3_ds2_m1]), + ]), + ] + + def test_multiple_model_versions_for_protocol_version(self, client, helpers, public_protocol, public_dataset): + protocol_version = public_protocol.repocache.latest_version + m1, m2 = recipes.model.make(_quantity=2) + m1v1 = helpers.add_cached_version(m1, visibility='public') + m1v2 = helpers.add_cached_version(m1, visibility='public') + m2v = helpers.add_cached_version(m2, visibility='public') + + # Create publicly visible fitting result versions + fit1_m1v1 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model=m1, + fittingresult__model_version=m1v1, + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + fit2_m1v2 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model=m1, + fittingresult__model_version=m1v2, + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + fit3_m2v = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model=m2, + fittingresult__model_version=m2v, + fittingresult__fittingspec_version__visibility='public', + ).fittingresult + + response = client.get( + '/entities/protocols/%d/versions/%s/fittings' % (public_protocol.id, protocol_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (public_dataset, [ + (m1, [fit2_m1v2, fit1_m1v1]), + (m2, [fit3_m2v]), + ]), + ] + + def test_ensure_private_results_are_not_shown_for_protocol_version(self, client, public_protocol): + protocol_version = public_protocol.repocache.latest_version + + recipes.fittingresult_version.make( + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model_version__visibility='private', + fittingresult__fittingspec_version__visibility='public', + fittingresult__dataset__visibility='public' + ) + + recipes.fittingresult_version.make( + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model_version__visibility='public', + fittingresult__fittingspec_version__visibility='private', + fittingresult__dataset__visibility='public' + ) + + recipes.fittingresult_version.make( + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + fittingresult__dataset__visibility='private' + ) + + fit = recipes.fittingresult_version.make( + fittingresult__protocol=public_protocol, + fittingresult__protocol_version=protocol_version, + fittingresult__model_version__visibility='public', + fittingresult__fittingspec_version__visibility='public', + fittingresult__dataset__visibility='public' + ).fittingresult + + response = client.get( + '/entities/protocols/%d/versions/%s/fittings' % (public_protocol.id, protocol_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (fit.dataset, [(fit.model, [fit])]), + ] + + def test_shows_fittings_related_to_fittingspec_version(self, client, fittingresult_version): + fit = fittingresult_version.fittingresult + + # should not be included, as it uses a different fittingspec + recipes.fittingresult_version.make() + + # should not be included, as it uses a different version of this fittingspec + recipes.fittingresult_version.make(fittingresult__fittingspec=fit.fittingspec) + + response = client.get( + '/fitting/specs/%d/versions/%s/fittings' % (fit.fittingspec.pk, fit.fittingspec_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [(fit.dataset, [(fit.model, [fit])])] + + def test_groups_by_dataset_for_fittingspec(self, client, helpers, public_fittingspec): + fittingspec_version = public_fittingspec.repocache.latest_version + ds1, ds2 = recipes.dataset.make(_quantity=2, visibility='public') + m1, m2 = recipes.model.make(_quantity=2) + m1v = helpers.add_cached_version(m1, visibility='public') + m2v = helpers.add_cached_version(m2, visibility='public') + + # Create publicly visible fitting result versions + fit1_ds1_m1 = recipes.fittingresult_version.make( + fittingresult__dataset=ds1, + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model=m1, + fittingresult__model_version=m1v, + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit2_ds1_m2 = recipes.fittingresult_version.make( + fittingresult__dataset=ds1, + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model=m2, + fittingresult__model_version=m2v, + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit3_ds2_m1 = recipes.fittingresult_version.make( + fittingresult__dataset=ds2, + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model=m1, + fittingresult__model_version=m1v, + fittingresult__protocol_version__visibility='public', + ).fittingresult + + response = client.get( + '/fitting/specs/%d/versions/%s/fittings' % (public_fittingspec.id, fittingspec_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (ds1, [ + (m1, [fit1_ds1_m1]), + (m2, [fit2_ds1_m2]), + ]), + (ds2, [ + (m1, [fit3_ds2_m1]), + ]), + ] + + def test_multiple_model_versions_for_fittingspec_version(self, client, helpers, public_fittingspec, public_dataset): + fittingspec_version = public_fittingspec.repocache.latest_version + m1, m2 = recipes.model.make(_quantity=2) + m1v1 = helpers.add_cached_version(m1, visibility='public') + m1v2 = helpers.add_cached_version(m1, visibility='public') + m2v = helpers.add_cached_version(m2, visibility='public') + + # Create publicly visible fitting result versions + fit1_m1v1 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model=m1, + fittingresult__model_version=m1v1, + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit2_m1v2 = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model=m1, + fittingresult__model_version=m1v2, + fittingresult__protocol_version__visibility='public', + ).fittingresult + + fit3_m2v = recipes.fittingresult_version.make( + fittingresult__dataset=public_dataset, + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model=m2, + fittingresult__model_version=m2v, + fittingresult__protocol_version__visibility='public', + ).fittingresult + + response = client.get( + '/fitting/specs/%d/versions/%s/fittings' % (public_fittingspec.id, fittingspec_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (public_dataset, [ + (m1, [fit2_m1v2, fit1_m1v1]), + (m2, [fit3_m2v]), + ]), + ] + + def test_ensure_private_results_are_not_shown_for_fittingspec_version(self, client, public_fittingspec): + fittingspec_version = public_fittingspec.repocache.latest_version + + recipes.fittingresult_version.make( + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model_version__visibility='private', + fittingresult__protocol_version__visibility='public', + fittingresult__dataset__visibility='public' + ) + + recipes.fittingresult_version.make( + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model_version__visibility='public', + fittingresult__protocol_version__visibility='private', + fittingresult__dataset__visibility='public' + ) + + recipes.fittingresult_version.make( + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model_version__visibility='public', + fittingresult__protocol_version__visibility='public', + fittingresult__dataset__visibility='private' + ) + + fit = recipes.fittingresult_version.make( + fittingresult__fittingspec=public_fittingspec, + fittingresult__fittingspec_version=fittingspec_version, + fittingresult__model_version__visibility='public', + fittingresult__protocol_version__visibility='public', + fittingresult__dataset__visibility='public' + ).fittingresult + + response = client.get( + '/fitting/specs/%d/versions/%s/fittings' % (public_fittingspec.id, fittingspec_version.sha) + ) + + assert response.status_code == 200 + assert response.context['comparisons'] == [ + (fit.dataset, [(fit.model, [fit])]), + ] + + @pytest.mark.django_db class TestEntityComparisonView: def test_loads_entity_versions(self, client, helpers, logged_in_user): diff --git a/weblab/entities/urls.py b/weblab/entities/urls.py index b3ea7cc72..f38c3476e 100644 --- a/weblab/entities/urls.py +++ b/weblab/entities/urls.py @@ -75,6 +75,12 @@ name='version_json', ), + url( + r'^%s/(?P\d+)/versions/%s/fittings$' % (_ENTITY_TYPE, _COMMIT), + views.EntityCompareFittingResultsView.as_view(), + name='compare_fittings', + ), + url( r'^%s/(?P\d+)/versions/%s/compare$' % (_ENTITY_TYPE, _COMMIT), views.EntityCompareExperimentsView.as_view(), diff --git a/weblab/entities/views.py b/weblab/entities/views.py index 989aa302d..c976d38fe 100644 --- a/weblab/entities/views.py +++ b/weblab/entities/views.py @@ -40,7 +40,7 @@ from accounts.forms import OwnershipTransferForm from core.visibility import Visibility, VisibilityMixin from experiments.models import Experiment, ExperimentVersion, PlannedExperiment -from fitting.models import FittingSpec +from fitting.models import FittingResult, FittingSpec from repocache.exceptions import RepoCacheMiss from repocache.models import CachedProtocolVersion @@ -89,6 +89,18 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) +class EntityVisibilityFormMixin: + def get_context_data(self, **kwargs): + entity = self._get_object() + visibility = entity.get_version_visibility(self.get_version().sha) + kwargs['form'] = EntityChangeVisibilityForm( + user=self.request.user, + initial={ + 'visibility': visibility, + }) + return super().get_context_data(**kwargs) + + class EntityVersionMixin(VisibilityMixin): """ Mixin for views describing a specific version of an `Entity` object @@ -201,23 +213,13 @@ def get_queryset(self): return self.model.objects.filter(author=self.request.user) -class EntityVersionView(EntityTypeMixin, EntityVersionMixin, DetailView): +class EntityVersionView(EntityTypeMixin, EntityVisibilityFormMixin, EntityVersionMixin, DetailView): """ View a version of an entity """ context_object_name = 'entity' template_name = 'entities/entity_version.html' - def get_context_data(self, **kwargs): - entity = self._get_object() - visibility = entity.get_version_visibility(self.get_version().sha) - kwargs['form'] = EntityChangeVisibilityForm( - user=self.request.user, - initial={ - 'visibility': visibility, - }) - return super().get_context_data(**kwargs) - class EntityVersionJsonView(EntityTypeMixin, EntityVersionMixin, SingleObjectMixin, View): def _planned_experiments(self, user): @@ -257,7 +259,7 @@ def get(self, request, *args, **kwargs): }) -class EntityCompareExperimentsView(EntityTypeMixin, EntityVersionMixin, DetailView): +class EntityCompareExperimentsView(EntityTypeMixin, EntityVisibilityFormMixin, EntityVersionMixin, DetailView): context_object_name = 'entity' template_name = 'entities/compare_experiments.html' @@ -291,6 +293,56 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) +class EntityCompareFittingResultsView(EntityTypeMixin, EntityVisibilityFormMixin, EntityVersionMixin, DetailView): + """ + List fitting results for this entity, with selection boxes for comparison + """ + context_object_name = 'entity' + template_name = 'entities/compare_fittings.html' + + def get_context_data(self, **kwargs): + entity = self._get_object() + version = self.get_version() + + entity_type = entity.entity_type + + fittings = FittingResult.objects.filter(**{ + entity_type: entity.pk, + entity_type + '_version': version.pk, + }).annotate( + version_count=Count('versions'), + ).filter( + version_count__gt=0, + ).select_related( + 'dataset', + 'model', + ).order_by('dataset', 'model', '-model_version__timestamp', '-protocol_version__timestamp') + + # Ensure all are visible to user + fittings = [ + fit for fit in fittings + if fit.is_visible_to_user(self.request.user) + ] + + # Group fittings by dataset and then model + # If the entity itself is a model, just group by dataset + def by_subgroup(fits): + if entity_type == 'model': + return list(fits) + else: + return [ + (obj, list(subfits)) + for (obj, subfits) in groupby(fits, lambda fit: fit.model) + ] + + kwargs['comparisons'] = [ + (obj, by_subgroup(fits)) + for (obj, fits) in groupby(fittings, lambda fit: fit.dataset) + ] + + return super().get_context_data(**kwargs) + + class EntityComparisonView(EntityTypeMixin, TemplateView): template_name = 'entities/compare.html' diff --git a/weblab/fitting/templatetags/__init__.py b/weblab/fitting/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weblab/fitting/templatetags/fittings.py b/weblab/fitting/templatetags/fittings.py new file mode 100644 index 000000000..8b946aced --- /dev/null +++ b/weblab/fitting/templatetags/fittings.py @@ -0,0 +1,18 @@ +from django import template +from django.core.urlresolvers import reverse + + +register = template.Library() + + +@register.simple_tag +def url_fitting_comparison_base(): + """ + Base URL for fitting result comparison page + """ + # Use dummy IDs to set up a comparison URL, then chop them off to + # get the base. This will be used by javascript to generate comparisons + # between fitting result versions. + url = reverse('fitting:result:compare', args=['/1/1']) + return url[:-4] + diff --git a/weblab/fitting/urls.py b/weblab/fitting/urls.py index 126106bfd..32e42a839 100644 --- a/weblab/fitting/urls.py +++ b/weblab/fitting/urls.py @@ -153,6 +153,12 @@ name='compare_json', ), + url( + r'^%s/(?P\d+)/versions/%s/fittings$' % (_ENTITY_TYPE, _COMMIT), + entity_views.EntityCompareFittingResultsView.as_view(), + name='compare_fittings', + ), + url( r'^%s/compare(?P(/\d+:%s){1,})(?:/show/%s)?$' % (_ENTITY_TYPE, _COMMIT, _FILEVIEW), entity_views.EntityComparisonView.as_view(), @@ -165,7 +171,6 @@ name='compare_json', ), - url( r'^%s/(?P\d+)/versions/%s/download/%s$' % (_ENTITY_TYPE, _COMMIT, _FILENAME), entity_views.EntityFileDownloadView.as_view(), diff --git a/weblab/static/js/entity.js b/weblab/static/js/entity.js index 7c09684ff..71e08b8e9 100644 --- a/weblab/static/js/entity.js +++ b/weblab/static/js/entity.js @@ -683,6 +683,21 @@ function init() { render (); } + function renderExperimentList() + { + if (!curVersion) { + var jsonUrl = $('#entityversion').data('version-json-href'); + $.getJSON(jsonUrl, function(data) { + notifications.display(data); + if (data.version) { + curVersion = data.version; + $('#entityversion').data('version-json', curVersion); + updateVersion(curVersion); + } + }); + } + } + function render () { @@ -806,6 +821,11 @@ function init() { render (); } + if (doc.version.experimentlist) { + window.onpopstate = renderExperimentList; + renderExperimentList(); + } + var $visibility = $(doc.version.visibility); $visibility.on( 'change', @@ -875,10 +895,12 @@ function init() { }); $("#entityexperimentlistpartnersactlatest").click(function () { $exp_list.children("li").children("input").prop('checked', true); + $exp_list.find(".latest-model-version").children("input").prop('checked', true); }); $("#entityexperimentlist_showallversions").click(function () { $(this).toggleClass("selected"); - $exp_list.find("ul").toggle(); + $exp_list.find(".older-model-version").toggle(); + $exp_list.find("ul.all-versions").toggle(); $("#entityexperimentlist_span_latest").toggle(); return false; }); @@ -894,6 +916,7 @@ function init() { }); $("#entityexperimentlist_span_latest").hide(); + $("#entityexperimentlistpartners .older-model-version").hide(); $(plugins).each(function(i, plugin) { visualizers[plugin.name] = plugin.get_visualizer() diff --git a/weblab/templates/datasets/base.html b/weblab/templates/datasets/base.html new file mode 100644 index 000000000..bfc0b1bad --- /dev/null +++ b/weblab/templates/datasets/base.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% load entities %} +{% load fittings %} +{% load staticfiles %} + +{% block content %} + {% include "./includes/dataset_header.html" %} +
+ +
+
+
+
+ + Created + by {{ dataset.author.full_name }}. + Visibility: {{ dataset.visibility }} + help. + +{% comment %} + {% can_delete_entity version as can_delete %} + {% if can_delete %} + Delete experiment version: + + delete this version of this experiment + {% endif %} +{% endcomment %} + +
Corresponding protocol: + {{ dataset.protocol.name }} + +
+
+ + +

+ {{ dataset.description }} +

+ +
+
+ + {% block content_detail %} + {% endblock content_detail %} +
+{% endblock content %} diff --git a/weblab/templates/datasets/compare_fittings.html b/weblab/templates/datasets/compare_fittings.html new file mode 100644 index 000000000..483d7abdc --- /dev/null +++ b/weblab/templates/datasets/compare_fittings.html @@ -0,0 +1,43 @@ +{% extends "datasets/base.html" %} +{% load entities %} +{% load fittings %} +{% load staticfiles %} + +{% block title %}Dataset : compare fitting experiments - {% endblock title %} + +{% block content_detail %} +
+

Fitting Experiments run with this dataset

+

+ The following models have been fit to this dataset. + Click to view the latest results of each single fitting experiment, or select multiple fitting experiments to compare them. +

+ +
+ {% for model, fittings in comparisons %} + {% with fittings.0.model_version as latest_model_version %} +

{{ model }}

+ + {% endwith %} + {% endfor %} +
+ +
+ [select all] + [select latest model versions] + [select none] +
+ + +
+
+{% endblock content_detail %} diff --git a/weblab/templates/datasets/dataset_detail.html b/weblab/templates/datasets/dataset_detail.html index 2743eb442..81e516783 100644 --- a/weblab/templates/datasets/dataset_detail.html +++ b/weblab/templates/datasets/dataset_detail.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "datasets/base.html" %} {% load staticfiles %} {% load datasets %} @@ -6,78 +6,31 @@ {% block body_id %}experiment-version{% endblock %} -{% block content %} - {% include "./includes/dataset_header.html" %} - -
- -
-
-
-
- - Created - by {{ dataset.author.full_name }}. - Visibility: {{ dataset.visibility }} - help. - -{% comment %} - {% can_delete_entity version as can_delete %} - {% if can_delete %} - Delete experiment version: - - delete this version of this experiment - {% endif %} -{% endcomment %} - -
Corresponding protocol: - {{ dataset.protocol.name }} - -
-
- - -

- {{ dataset.description }} -

- -
-{% comment %} - -
-{% endcomment %} -
- -
-
- -

-
Created by - .
-
-
+{% block content_detail %} +
+
+ +

+
Created by + .
+
+
-
-

Files in this dataset

-

- -
- - Download - Download archive of all files - -
-
-
+
+

Files in this dataset

+

+ +
+ + Download + Download archive of all files + +
-{% endblock %} +
+{% endblock content_detail %} diff --git a/weblab/templates/datasets/includes/dataset_header.html b/weblab/templates/datasets/includes/dataset_header.html index 7d767bd80..d5adb4411 100644 --- a/weblab/templates/datasets/includes/dataset_header.html +++ b/weblab/templates/datasets/includes/dataset_header.html @@ -2,7 +2,9 @@ {% load datasets %}

+ Dataset: {{ dataset.name }} +

diff --git a/weblab/templates/entities/compare_experiments.html b/weblab/templates/entities/compare_experiments.html index 9f328ddcc..d2a16b4bd 100644 --- a/weblab/templates/entities/compare_experiments.html +++ b/weblab/templates/entities/compare_experiments.html @@ -27,7 +27,7 @@

Experiments using this {{type}}

{% endif %} {% if experiments|length > 1 %} -
    +
{% block content_detail %}