diff --git a/weblab/conftest.py b/weblab/conftest.py
index 0cff5a104..0cc618f05 100644
--- a/weblab/conftest.py
+++ b/weblab/conftest.py
@@ -13,6 +13,7 @@
from core import recipes
from datasets.models import Dataset
from entities.models import Entity
+from fitting.models import FittingResult
from repocache.populate import populate_entity_cache
@@ -43,8 +44,11 @@ def add_version(entity,
return commit
@staticmethod
- def cached_version(entity, **kwargs):
- """Add a single commit/version to an entity and return the relevant repocache entry"""
+ def add_cached_version(entity, **kwargs):
+ """
+ Add a single commit/version to an entity along with a repocache entry.
+ @return the relevant repocache entry
+ """
assert kwargs.get('cache', True), "Cache must be true for cached version"
version = Helpers.add_version(entity, **kwargs)
return entity.repocache.get_version(version.sha)
@@ -99,6 +103,17 @@ def add_permission(user, perm, model=Entity):
def login(client, user):
client.login(username=user.email, password='password')
+ @staticmethod
+ def link_to_protocol(protocol, *objects):
+ """
+ Link given objects to protocol (fitting specs or datasets)
+ @param protocol - protocol to link to
+ @param objects - list of objects to link to the protocol
+ """
+ for obj in objects:
+ obj.protocol = protocol
+ obj.save()
+
@pytest.fixture
def helpers():
@@ -156,28 +171,28 @@ def fittingspec_with_version():
@pytest.fixture
def public_model(helpers):
- model = recipes.model.make()
+ model = recipes.model.make(name='public model')
helpers.add_version(model, visibility='public')
return model
@pytest.fixture
def public_protocol(helpers):
- protocol = recipes.protocol.make()
+ protocol = recipes.protocol.make(name='public protocol')
helpers.add_version(protocol, visibility='public')
return protocol
@pytest.fixture
def public_fittingspec(helpers):
- fittingspec = recipes.fittingspec.make()
+ fittingspec = recipes.fittingspec.make(name='public fitting spec')
helpers.add_version(fittingspec, visibility='public')
return fittingspec
@pytest.fixture
-def public_dataset(helpers):
- dataset = recipes.dataset.make(visibility='public')
+def public_dataset():
+ dataset = recipes.dataset.make(visibility='public', name='public dataset')
return dataset
@@ -195,6 +210,32 @@ def moderated_protocol(helpers):
return protocol
+@pytest.fixture
+def private_model(helpers):
+ model = recipes.model.make(name='private model')
+ helpers.add_version(model, visibility='private')
+ return model
+
+
+@pytest.fixture
+def private_protocol(helpers):
+ protocol = recipes.protocol.make(name='private protocol')
+ helpers.add_version(protocol, visibility='private')
+ return protocol
+
+
+@pytest.fixture
+def private_fittingspec(helpers):
+ fittingspec = recipes.fittingspec.make(name='private fittingspec')
+ helpers.add_version(fittingspec, visibility='private')
+ return fittingspec
+
+
+@pytest.fixture
+def private_dataset():
+ return recipes.dataset.make(name='private dataset', visibility='private')
+
+
@pytest.fixture
def queued_experiment(model_with_version, protocol_with_version):
version = recipes.experiment_version.make(
@@ -208,6 +249,22 @@ def queued_experiment(model_with_version, protocol_with_version):
return version
+@pytest.fixture
+def queued_fittingresult(public_model, public_protocol, public_fittingspec, public_dataset):
+ version = recipes.fittingresult_version.make(
+ status='QUEUED',
+ fittingresult__model=public_model,
+ fittingresult__model_version=public_model.repocache.latest_version,
+ fittingresult__protocol=public_protocol,
+ fittingresult__protocol_version=public_protocol.repocache.latest_version,
+ fittingresult__fittingspec=public_fittingspec,
+ fittingresult__fittingspec_version=public_fittingspec.repocache.latest_version,
+ fittingresult__dataset=public_dataset,
+ )
+ recipes.running_experiment.make(runnable=version)
+ return version
+
+
@pytest.fixture
def experiment_with_result(model_with_version, protocol_with_version):
version = recipes.experiment_version.make(
@@ -323,6 +380,18 @@ def moderator(user, helpers):
return user
+@pytest.fixture
+def fits_user(logged_in_user):
+ """User with permission to run fittings"""
+ content_type = ContentType.objects.get_for_model(FittingResult)
+ permission = Permission.objects.get(
+ codename='run_fits',
+ content_type=content_type,
+ )
+ logged_in_user.user_permissions.add(permission)
+ return logged_in_user
+
+
@pytest.fixture
def dataset_creator(user, helpers):
helpers.add_permission(user, 'create_dataset', Dataset)
diff --git a/weblab/core/recipes.py b/weblab/core/recipes.py
index 1b289b485..88f4d0d8a 100644
--- a/weblab/core/recipes.py
+++ b/weblab/core/recipes.py
@@ -22,17 +22,17 @@
analysis_task = Recipe('AnalysisTask', entity=foreign_key(protocol))
-cached_model = Recipe('CachedModel')
-cached_model_version = Recipe('CachedModelVersion')
-cached_model_tag = Recipe('CachedModelTag')
+cached_model = Recipe('CachedModel', entity=foreign_key(model))
+cached_model_version = Recipe('CachedModelVersion', entity=foreign_key(cached_model))
+cached_model_tag = Recipe('CachedModelTag', entity=foreign_key(cached_model))
-cached_protocol = Recipe('CachedProtocol')
-cached_protocol_version = Recipe('CachedProtocolVersion')
-cached_protocol_tag = Recipe('CachedProtocolTag')
+cached_protocol = Recipe('CachedProtocol', entity=foreign_key(protocol))
+cached_protocol_version = Recipe('CachedProtocolVersion', entity=foreign_key(cached_protocol))
+cached_protocol_tag = Recipe('CachedProtocolTag', entity=foreign_key(cached_protocol))
-cached_fittingspec = Recipe('CachedFittingSpec')
-cached_fittingspec_version = Recipe('CachedFittingSpecVersion')
-cached_fittingspec_tag = Recipe('CachedFittingSpecTag')
+cached_fittingspec = Recipe('CachedFittingSpec', entity=foreign_key(fittingspec))
+cached_fittingspec_version = Recipe('CachedFittingSpecVersion', entity=foreign_key(cached_fittingspec))
+cached_fittingspec_tag = Recipe('CachedFittingSpecTag', entity=foreign_key(cached_fittingspec))
experiment = Recipe(
'Experiment',
@@ -48,7 +48,6 @@
running_experiment = Recipe('RunningExperiment', runnable=foreign_key(runnable))
-
dataset = Recipe('Dataset',
name=seq('my dataset'),
protocol=foreign_key(protocol))
diff --git a/weblab/datasets/tests/test_views.py b/weblab/datasets/tests/test_views.py
index 6ef75cf97..56e8fa7a9 100644
--- a/weblab/datasets/tests/test_views.py
+++ b/weblab/datasets/tests/test_views.py
@@ -400,6 +400,21 @@ def test_returns_404_for_nonexistent_file(self, my_dataset_with_file, client):
@pytest.mark.django_db
class TestDatasetArchiveView:
+ def test_anonymous_dataset_download_for_running_fittingresult(
+ self, client, queued_fittingresult, my_dataset_with_file,
+ ):
+ queued_fittingresult.fittingresult.dataset = my_dataset_with_file
+ queued_fittingresult.fittingresult.save()
+
+ response = client.get(
+ '/datasets/%d/archive' % my_dataset_with_file.pk,
+ HTTP_AUTHORIZATION='Token {}'.format(queued_fittingresult.signature)
+ )
+
+ assert response.status_code == 200
+ archive = zipfile.ZipFile(BytesIO(response.content))
+ assert archive.filelist[0].filename == 'mydataset.csv'
+
def test_download_archive(self, my_dataset_with_file, client):
response = client.get('/datasets/%d/archive' % my_dataset_with_file.pk)
assert response.status_code == 200
diff --git a/weblab/datasets/views.py b/weblab/datasets/views.py
index cfa03e9fc..3c506c0da 100644
--- a/weblab/datasets/views.py
+++ b/weblab/datasets/views.py
@@ -200,6 +200,17 @@ class DatasetArchiveView(VisibilityMixin, SingleObjectMixin, View):
"""
model = Dataset
+ def check_access_token(self, token):
+ """
+ Override to allow token based access to dataset archive downloads -
+ must match a (fitting) `RunningExperiment` set up against the dataset.
+ """
+ from experiments.models import RunningExperiment
+ return RunningExperiment.objects.filter(
+ id=token,
+ runnable__fittingresultversion__fittingresult__dataset=self.get_object().id,
+ ).exists()
+
def get_archive_name(self, dataset):
return dataset.archive_name
diff --git a/weblab/entities/tests/test_views.py b/weblab/entities/tests/test_views.py
index 7457aef68..62864e407 100644
--- a/weblab/entities/tests/test_views.py
+++ b/weblab/entities/tests/test_views.py
@@ -1786,6 +1786,27 @@ def test_anonymous_protocol_download_for_running_experiment(self, client, queued
archive = zipfile.ZipFile(BytesIO(response.content))
assert archive.filelist[0].filename == 'file1.txt'
+ @pytest.mark.parametrize("entity_type,url_fragment", [
+ ('model', '/entities/models'),
+ ('protocol', '/entities/protocols'),
+ ('fittingspec', '/fitting/specs'),
+ ])
+ def test_anonymous_entity_download_for_running_fittingresult(
+ self, client, queued_fittingresult, entity_type, url_fragment
+ ):
+ entity = getattr(queued_fittingresult.fittingresult, entity_type)
+ sha = entity.repo.latest_commit.sha
+ entity.set_version_visibility(sha, 'private')
+
+ response = client.get(
+ '%s/%d/versions/latest/archive' % (url_fragment, entity.pk),
+ HTTP_AUTHORIZATION='Token {}'.format(queued_fittingresult.signature)
+ )
+
+ assert response.status_code == 200
+ archive = zipfile.ZipFile(BytesIO(response.content))
+ assert archive.filelist[0].filename == 'file1.txt'
+
def test_anonymous_protocol_download_for_analysis_task(self, client, analysis_task):
protocol = analysis_task.entity
protocol.set_version_visibility('latest', 'private')
diff --git a/weblab/entities/views.py b/weblab/entities/views.py
index 43375f667..32578879b 100644
--- a/weblab/entities/views.py
+++ b/weblab/entities/views.py
@@ -661,16 +661,30 @@ class EntityArchiveView(SingleObjectMixin, EntityVersionMixin, View):
def check_access_token(self, token):
"""
Override to allow token based access to entity archive downloads -
- must match a `RunningExperiment` or `AnalysisTask` object set up against the entity
+ must match a `RunningExperiment` or `AnalysisTask` object set up against the entity.
+
+ We support both simulation and fitting experiments.
"""
from entities.models import AnalysisTask
from experiments.models import RunningExperiment
- entity_field = 'runnable__experimentversion__experiment__%s' % self.kwargs['entity_type']
self_id = self._get_object().id
- return (RunningExperiment.objects.filter(
- id=token,
- **{entity_field: self_id}
- ).exists() or AnalysisTask.objects.filter(id=token, entity=self_id).exists())
+ if AnalysisTask.objects.filter(id=token, entity=self_id).exists():
+ return True
+
+ query_tpl = 'runnable__{subclass}version__{subclass}__{entity_type}'
+ entity_type = self.kwargs['entity_type']
+
+ # Look for all experiments linked to the given entity
+ # Fitting specs are not valid for simulation experiments, and go by
+ # a different field name for fitting experiments.
+ q_expt = RunningExperiment.objects.none()
+ if entity_type == 'spec':
+ entity_type = 'fittingspec'
+ else:
+ q_expt |= Q(**{query_tpl.format(subclass='experiment', entity_type=entity_type): self_id})
+
+ q_expt |= Q(**{query_tpl.format(subclass='fittingresult', entity_type=entity_type): self_id})
+ return RunningExperiment.objects.filter(Q(id=token) & q_expt).exists()
def get(self, request, *args, **kwargs):
entity = self._get_object()
diff --git a/weblab/experiments/processing.py b/weblab/experiments/processing.py
index 3bf362941..8f560edd1 100644
--- a/weblab/experiments/processing.py
+++ b/weblab/experiments/processing.py
@@ -100,7 +100,7 @@ def submit_runnable(runnable, body, user):
runnable.save()
-def submit_experiment(model, model_version, protocol, protocol_version, user, rerun_ok):
+def submit_experiment(model_version, protocol_version, user, rerun_ok):
"""Submit a Celery task to run an experiment.
@param rerun_ok if False and an ExperimentVersion already exists, will just return that.
@@ -108,10 +108,10 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re
@return the ExperimentVersion for the run
"""
experiment, _ = Experiment.objects.get_or_create(
- model=model,
- protocol=protocol,
- model_version=model.repocache.get_version(model_version),
- protocol_version=protocol.repocache.get_version(protocol_version),
+ model=model_version.model,
+ protocol=protocol_version.protocol,
+ model_version=model_version,
+ protocol_version=protocol_version,
defaults={
'author': user,
}
@@ -138,17 +138,17 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re
model_url = reverse(
'entities:entity_archive',
- args=['model', model.pk, model_version]
+ args=['model', model_version.model.pk, model_version.sha]
)
protocol_url = reverse(
'entities:entity_archive',
- args=['protocol', protocol.pk, protocol_version]
+ args=['protocol', protocol_version.protocol.pk, protocol_version.sha]
)
body = {
'model': urljoin(settings.CALLBACK_BASE_URL, model_url),
'protocol': urljoin(settings.CALLBACK_BASE_URL, protocol_url),
}
- if protocol.is_fitting_spec:
+ if protocol_version.protocol.is_fitting_spec:
body['dataset'] = body['fittingSpec'] = body['protocol']
submit_runnable(version, body, user)
diff --git a/weblab/experiments/tests/test_processing.py b/weblab/experiments/tests/test_processing.py
index 2820a9df6..193088b58 100644
--- a/weblab/experiments/tests/test_processing.py
+++ b/weblab/experiments/tests/test_processing.py
@@ -39,13 +39,13 @@ def test_creates_new_experiment_and_side_effects(
user, model_with_version, protocol_with_version):
model = model_with_version
protocol = protocol_with_version
- model_version = model.repo.latest_commit.sha
- protocol_version = protocol.repo.latest_commit.sha
+ model_version = model.repocache.latest_version
+ protocol_version = protocol.repocache.latest_version
assert Experiment.objects.count() == 0
assert RunningExperiment.objects.count() == 0
- version, is_new = submit_experiment(model, model_version, protocol, protocol_version, user, False)
+ version, is_new = submit_experiment(model_version, protocol_version, user, False)
assert is_new
# Check properties of the new experiment & version
@@ -53,16 +53,16 @@ def test_creates_new_experiment_and_side_effects(
assert version.experiment.model == model
assert version.experiment.protocol == protocol
assert version.author == user
- assert version.experiment.model_version.sha == model_version
- assert version.experiment.protocol_version.sha == protocol_version
+ assert version.experiment.model_version == model_version
+ assert version.experiment.protocol_version == protocol_version
assert version.experiment.author == user
assert version.status == ExperimentVersion.STATUS_QUEUED
# Check it did submit to the webservice
- model_url = '/entities/models/%d/versions/%s/archive' % (model.pk, model_version)
+ model_url = '/entities/models/%d/versions/%s/archive' % (model.pk, model_version.sha)
protocol_url = (
'/entities/protocols/%d/versions/%s/archive' %
- (protocol.pk, protocol_version))
+ (protocol.pk, protocol_version.sha))
assert mock_post.call_count == 1
assert mock_post.call_args[0][0] == settings.CHASTE_URL
@@ -106,8 +106,7 @@ def test_uses_existing_experiment(self, mock_post,
protocol=protocol,
protocol_version=protocol_version)
- version, is_new = submit_experiment(model, model_version.sha,
- protocol, protocol_version.sha, user, False)
+ version, is_new = submit_experiment(model_version, protocol_version, user, False)
assert is_new
assert version.experiment == experiment
@@ -121,7 +120,7 @@ def test_raises_exception_on_webservice_error(self, mock_post,
mock_post.side_effect = generate_response('something %s')
with pytest.raises(ProcessingException):
- submit_experiment(model, model_version.sha, protocol, protocol_version.sha, user, False)
+ submit_experiment(model_version, protocol_version, user, False)
# There should be no running experiment
assert RunningExperiment.objects.count() == 0
@@ -144,7 +143,7 @@ def test_records_submission_error(self, mock_post,
mock_post.side_effect = generate_response('%s an error occurred')
- version, is_new = submit_experiment(model, model_version.sha, protocol, protocol_version.sha, user, False)
+ version, is_new = submit_experiment(model_version, protocol_version, user, False)
assert is_new
assert version.status == ExperimentVersion.STATUS_FAILED
@@ -160,7 +159,7 @@ def test_records_inapplicable_result(self, mock_post,
mock_post.side_effect = generate_response('%s inapplicable')
- version, is_new = submit_experiment(model, model_version.sha, protocol, protocol_version.sha, user, False)
+ version, is_new = submit_experiment(model_version, protocol_version, user, False)
assert is_new
assert version.status == ExperimentVersion.STATUS_INAPPLICABLE
diff --git a/weblab/experiments/views.py b/weblab/experiments/views.py
index 0514d8784..bd4fa0dae 100644
--- a/weblab/experiments/views.py
+++ b/weblab/experiments/views.py
@@ -314,22 +314,24 @@ def post(self, request, *args, **kwargs):
exp = exp_ver.experiment
model = exp.model
protocol = exp.protocol
- model_version = exp.model_version.sha
- protocol_version = exp.protocol_version.sha
+ model_sha = exp.model_version.sha
+ protocol_sha = exp.protocol_version.sha
else:
model = get_object_or_404(ModelEntity, pk=request.POST['model'])
protocol = get_object_or_404(ProtocolEntity, pk=request.POST['protocol'])
- model_version = request.POST['model_version']
- protocol_version = request.POST['protocol_version']
+ model_sha = request.POST['model_version']
+ protocol_sha = request.POST['protocol_version']
- version, is_new = submit_experiment(model, model_version, protocol, protocol_version,
+ model_version = model.repocache.get_version(model_sha)
+ protocol_version = protocol.repocache.get_version(protocol_sha)
+ version, is_new = submit_experiment(model_version, protocol_version,
request.user, 'rerun' in request.POST or 'planned' in request.POST)
queued = version.status == ExperimentVersion.STATUS_QUEUED
if is_new and version.status != ExperimentVersion.STATUS_FAILED:
# Remove from planned experiments
PlannedExperiment.objects.filter(
- model=model, model_version=model_version,
- protocol=protocol, protocol_version=protocol_version
+ model=model, model_version=model_sha,
+ protocol=protocol, protocol_version=protocol_sha
).delete()
version_url = reverse('experiments:version',
diff --git a/weblab/fitting/forms.py b/weblab/fitting/forms.py
index 4e239c67f..6c7073eee 100644
--- a/weblab/fitting/forms.py
+++ b/weblab/fitting/forms.py
@@ -1,8 +1,13 @@
+from braces.forms import UserKwargModelFormMixin
+from django import forms
+from django.core.exceptions import ValidationError
+from datasets.models import Dataset
from entities.forms import EntityForm, EntityRenameForm, EntityVersionForm
-from entities.models import ProtocolEntity
+from entities.models import ModelEntity, ProtocolEntity
+from repocache.models import CachedFittingSpecVersion, CachedModelVersion, CachedProtocolVersion
-from .models import FittingSpec
+from .models import FittingResult, FittingSpec
class FittingSpecForm(EntityForm):
@@ -32,3 +37,68 @@ class FittingSpecRenameForm(EntityRenameForm):
class Meta:
model = FittingSpec
fields = ['name']
+
+
+class VersionChoiceField(forms.ModelChoiceField):
+ def label_from_instance(self, obj):
+ return obj.nice_version()
+
+
+class FittingResultCreateForm(UserKwargModelFormMixin, forms.ModelForm):
+ """Used for creating and running a new fitting result"""
+ model_version = VersionChoiceField(queryset=CachedModelVersion.objects.all())
+ protocol_version = VersionChoiceField(queryset=CachedProtocolVersion.objects.all())
+ fittingspec_version = VersionChoiceField(queryset=CachedFittingSpecVersion.objects.all(),
+ label='Fitting specification version')
+
+ class Meta:
+ model = FittingResult
+ fields = ('model', 'model_version', 'protocol', 'protocol_version',
+ 'fittingspec', 'fittingspec_version', 'dataset')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['fittingspec'].label = 'Fitting specification'
+
+ # Disable fields with preselected values
+ # But only when there is no data bound - otherwise we will lose the
+ # posted values of these fields from the submitted form.
+ if not self.is_bound:
+ self.fields['model'].disabled = bool(self.initial.get('model'))
+ self.fields['protocol'].disabled = bool(self.initial.get('protocol'))
+ self.fields['fittingspec'].disabled = bool(self.initial.get('fittingspec'))
+ self.fields['dataset'].disabled = bool(self.initial.get('dataset'))
+
+ # Ensure only visible entities and versions are available to user
+ self.fields['model'].queryset = ModelEntity.objects.visible_to_user(self.user)
+ self.fields['protocol'].queryset = ProtocolEntity.objects.visible_to_user(self.user)
+ self.fields['fittingspec'].queryset = FittingSpec.objects.visible_to_user(self.user)
+ self.fields['dataset'].queryset = Dataset.objects.visible_to_user(self.user)
+
+ self.fields['model_version'].queryset = CachedModelVersion.objects.visible_to_user(self.user)
+ self.fields['protocol_version'].queryset = CachedProtocolVersion.objects.visible_to_user(self.user)
+ self.fields['fittingspec_version'].queryset = CachedFittingSpecVersion.objects.visible_to_user(self.user)
+
+ def clean(self):
+ # Check ownership of versions by entities
+ model_version = self.cleaned_data.get('model_version')
+ if model_version and model_version.model != self.cleaned_data.get('model'):
+ raise ValidationError({'model_version': 'Model version must belong to model'})
+
+ protocol_version = self.cleaned_data.get('protocol_version')
+ if protocol_version and protocol_version.protocol != self.cleaned_data['protocol']:
+ raise ValidationError({'protocol_version': 'Protocol version must belong to protocol'})
+
+ fittingspec_version = self.cleaned_data.get('fittingspec_version')
+ if fittingspec_version and fittingspec_version.fittingspec != self.cleaned_data['fittingspec']:
+ raise ValidationError({'fittingspec_version': 'Fitting spec version must belong to fitting spec'})
+
+ # Check linkage between protocols, datasets and fitting specs
+ protocol = self.cleaned_data.get('protocol')
+ dataset = self.cleaned_data.get('dataset')
+ if dataset and dataset.protocol != protocol:
+ raise ValidationError({'protocol': 'Protocol and dataset must match'})
+
+ fittingspec = self.cleaned_data.get('fittingspec')
+ if fittingspec and fittingspec.protocol != protocol:
+ raise ValidationError({'protocol': 'Protocol and fitting spec must match'})
diff --git a/weblab/fitting/models.py b/weblab/fitting/models.py
index cd8a497f8..9c73eb976 100644
--- a/weblab/fitting/models.py
+++ b/weblab/fitting/models.py
@@ -82,6 +82,11 @@ class FittingResult(ExperimentMixin, UserCreatedModelMixin, models.Model):
default=None, null=False, related_name='fit_ver_fitres',
)
+ @property
+ def nice_fittingspec_version(self):
+ """Use tags to give a nicer representation of the commit id"""
+ return self.fittingspec_version.nice_version()
+
class Meta:
unique_together = ('fittingspec', 'dataset', 'model', 'protocol',
'fittingspec_version', 'model_version', 'protocol_version')
diff --git a/weblab/fitting/processing.py b/weblab/fitting/processing.py
index d504ddb7c..1256ae012 100644
--- a/weblab/fitting/processing.py
+++ b/weblab/fitting/processing.py
@@ -14,9 +14,9 @@
def submit_fitting(
- model, model_version,
- protocol, protocol_version,
- fittingspec, fittingspec_version,
+ model_version,
+ protocol_version,
+ fittingspec_version,
dataset, user, rerun_ok,
):
"""Submit a Celery task to run a fitting experiment
@@ -26,13 +26,13 @@ def submit_fitting(
@return the FittingResultVersion for the run
"""
fittingresult, _ = FittingResult.objects.get_or_create(
- model=model,
- protocol=protocol,
- fittingspec=fittingspec,
+ model=model_version.model,
+ protocol=protocol_version.protocol,
+ fittingspec=fittingspec_version.fittingspec,
dataset=dataset,
- model_version=model.repocache.get_version(model_version),
- protocol_version=protocol.repocache.get_version(protocol_version),
- fittingspec_version=fittingspec.repocache.get_version(fittingspec_version),
+ model_version=model_version,
+ protocol_version=protocol_version,
+ fittingspec_version=fittingspec_version,
defaults={
'author': user,
}
@@ -59,15 +59,15 @@ def submit_fitting(
model_url = reverse(
'entities:entity_archive',
- args=['model', model.pk, model_version]
+ args=['model', model_version.model.pk, model_version.sha]
)
protocol_url = reverse(
'entities:entity_archive',
- args=['protocol', protocol.pk, protocol_version]
+ args=['protocol', protocol_version.protocol.pk, protocol_version.sha]
)
fittingspec_url = reverse(
'fitting:entity_archive',
- args=['spec', fittingspec.pk, fittingspec_version]
+ args=['spec', fittingspec_version.fittingspec.pk, fittingspec_version.sha]
)
dataset_url = reverse(
'datasets:archive',
diff --git a/weblab/fitting/tests/test_forms.py b/weblab/fitting/tests/test_forms.py
new file mode 100644
index 000000000..839ae466d
--- /dev/null
+++ b/weblab/fitting/tests/test_forms.py
@@ -0,0 +1,247 @@
+import pytest
+
+from core import recipes
+from fitting.forms import FittingResultCreateForm
+
+
+@pytest.mark.django_db
+class TestFittingResultCreateForm:
+ def test_fields_exist(self, fits_user):
+ form = FittingResultCreateForm(user=fits_user)
+ assert 'model' in form.fields
+ assert 'model_version' in form.fields
+ assert 'protocol' in form.fields
+ assert 'protocol_version' in form.fields
+ assert 'fittingspec' in form.fields
+ assert 'fittingspec_version' in form.fields
+ assert 'dataset' in form.fields
+
+ def test_fields_enabled_by_default(self, fits_user):
+ form = FittingResultCreateForm(user=fits_user)
+ assert not form.fields['model'].disabled
+ assert not form.fields['protocol'].disabled
+ assert not form.fields['fittingspec'].disabled
+ assert not form.fields['dataset'].disabled
+
+ def test_disables_preselected_model(self, public_model, fits_user):
+ form = FittingResultCreateForm(initial={'model': public_model.pk}, user=fits_user)
+ assert form.fields['model'].disabled
+
+ def test_disables_preselected_protocol(self, public_protocol, fits_user):
+ form = FittingResultCreateForm(initial={'protocol': public_protocol.pk}, user=fits_user)
+ assert form.fields['protocol'].disabled
+
+ def test_disables_preselected_fittingspec(self, public_fittingspec, fits_user):
+ form = FittingResultCreateForm(initial={'fittingspec': public_fittingspec.pk}, user=fits_user)
+ assert form.fields['fittingspec'].disabled
+
+ def test_disables_preselected_dataset(self, public_dataset, fits_user):
+ form = FittingResultCreateForm(initial={'dataset': public_dataset.pk}, user=fits_user)
+ assert form.fields['dataset'].disabled
+
+ def test_valid_form(
+ self, public_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, public_dataset, public_fittingspec)
+
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert form.is_valid()
+
+ def test_model_version_must_be_visible(
+ self, private_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, public_dataset, public_fittingspec)
+
+ form = FittingResultCreateForm({
+ 'model': private_model.pk,
+ 'model_version': private_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'model_version' in form.errors
+ assert form.errors['model_version'][0].startswith("Select a valid choice.")
+
+ def test_protocol_version_must_be_visible(
+ self, public_model, private_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(private_protocol, public_dataset, public_fittingspec)
+
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': private_protocol.pk,
+ 'protocol_version': private_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'protocol_version' in form.errors
+ assert form.errors['protocol_version'][0].startswith("Select a valid choice.")
+
+ def test_fittingspec_version_must_be_visible(
+ self, public_model, public_protocol,
+ private_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, public_dataset, private_fittingspec)
+
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': private_fittingspec.pk,
+ 'fittingspec_version': private_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'fittingspec_version' in form.errors
+ assert form.errors['fittingspec_version'][0].startswith("Select a valid choice.")
+
+ def test_dataset_must_be_visible(
+ self, public_model, public_protocol, public_fittingspec, private_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, private_dataset, public_fittingspec)
+
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': private_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'dataset' in form.errors
+ assert form.errors['dataset'][0].startswith("Select a valid choice.")
+
+ def test_only_shows_visible_models(self, private_model, public_model, fits_user):
+ form = FittingResultCreateForm(user=fits_user)
+ assert form.fields['model'].valid_value(public_model.pk)
+ assert not form.fields['model'].valid_value(private_model.pk)
+
+ def test_only_shows_visible_protocols(self, private_protocol, public_protocol, fits_user):
+ form = FittingResultCreateForm(user=fits_user)
+ assert form.fields['protocol'].valid_value(public_protocol.pk)
+ assert not form.fields['protocol'].valid_value(private_protocol.pk)
+
+ def test_only_shows_visible_fittingspecs(self, private_fittingspec, public_fittingspec, fits_user):
+ form = FittingResultCreateForm(user=fits_user)
+ assert form.fields['fittingspec'].valid_value(public_fittingspec.pk)
+ assert not form.fields['fittingspec'].valid_value(private_fittingspec.pk)
+
+ def test_only_shows_visible_datasets(self, private_dataset, public_dataset, fits_user):
+ form = FittingResultCreateForm(user=fits_user)
+ assert form.fields['dataset'].valid_value(public_dataset.pk)
+ assert not form.fields['dataset'].valid_value(private_dataset.pk)
+
+ def test_model_version_must_belong_to_model(
+ self, public_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, public_dataset, public_fittingspec)
+
+ invalid_version = recipes.cached_model_version.make()
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': invalid_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'model_version' in form.errors
+
+ def test_protocol_version_must_belong_to_protocol(
+ self, public_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, public_dataset, public_fittingspec)
+
+ invalid_version = recipes.cached_protocol_version.make()
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': invalid_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'protocol_version' in form.errors
+
+ def test_fittingspec_version_must_belong_to_fittingspec(
+ self, public_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+
+ helpers.link_to_protocol(public_protocol, public_dataset, public_fittingspec)
+
+ invalid_version = recipes.cached_fittingspec_version.make()
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': invalid_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'fittingspec_version' in form.errors
+
+ def test_protocol_and_fittingspec_must_be_linked(
+ self, public_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+ helpers.link_to_protocol(public_protocol, public_dataset)
+
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'protocol' in form.errors
+
+ def test_protocol_and_dataset_must_be_linked(
+ self, public_model, public_protocol,
+ public_fittingspec, public_dataset, fits_user, helpers
+ ):
+
+ helpers.link_to_protocol(public_protocol, public_fittingspec)
+
+ form = FittingResultCreateForm({
+ 'model': public_model.pk,
+ 'model_version': public_model.repocache.latest_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': public_protocol.repocache.latest_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': public_fittingspec.repocache.latest_version.pk,
+ 'dataset': public_dataset.pk,
+ }, user=fits_user)
+ assert not form.is_valid()
+ assert 'protocol' in form.errors
diff --git a/weblab/fitting/tests/test_models.py b/weblab/fitting/tests/test_models.py
index 3835b3914..3aa77633a 100644
--- a/weblab/fitting/tests/test_models.py
+++ b/weblab/fitting/tests/test_models.py
@@ -139,6 +139,7 @@ def test_nice_versions(self, fittingresult_version):
assert fitres.nice_model_version == fitres.model.repocache.latest_version.sha[:8] + '...'
assert fitres.nice_protocol_version == fitres.protocol.repocache.latest_version.sha[:8] + '...'
+ assert fitres.nice_fittingspec_version == fitres.fittingspec.repocache.latest_version.sha[:8] + '...'
fitres.model.repo.tag('v1')
populate_entity_cache(fitres.model)
@@ -146,9 +147,12 @@ def test_nice_versions(self, fittingresult_version):
fitres.protocol.repo.tag('v2')
populate_entity_cache(fitres.protocol)
-
assert fitres.nice_protocol_version == 'v2'
+ fitres.fittingspec.repo.tag('v3')
+ populate_entity_cache(fitres.fittingspec)
+ assert fitres.nice_fittingspec_version == 'v3'
+
def test_visibility(self, helpers):
model = recipes.model.make()
protocol = recipes.protocol.make()
@@ -156,12 +160,12 @@ def test_visibility(self, helpers):
ds2 = recipes.dataset.make(visibility='public')
fittingspec = recipes.fittingspec.make()
- mv1 = helpers.cached_version(model, visibility='private')
- mv2 = helpers.cached_version(model, visibility='public')
- pv1 = helpers.cached_version(protocol, visibility='private')
- pv2 = helpers.cached_version(protocol, visibility='public')
- fv1 = helpers.cached_version(fittingspec, visibility='private')
- fv2 = helpers.cached_version(fittingspec, visibility='public')
+ mv1 = helpers.add_cached_version(model, visibility='private')
+ mv2 = helpers.add_cached_version(model, visibility='public')
+ pv1 = helpers.add_cached_version(protocol, visibility='private')
+ pv2 = helpers.add_cached_version(protocol, visibility='public')
+ fv1 = helpers.add_cached_version(fittingspec, visibility='private')
+ fv2 = helpers.add_cached_version(fittingspec, visibility='public')
# all public
assert recipes.fittingresult.make(
@@ -224,9 +228,9 @@ def test_viewers(self, helpers, user):
# (https://github.com/ModellingWebLab/WebLab/issues/247)
# so test with a public dataset for now
dataset = recipes.dataset.make(visibility='public')
- mv = helpers.cached_version(model, visibility='private')
- pv = helpers.cached_version(protocol, visibility='private')
- fv = helpers.cached_version(fittingspec, visibility='private')
+ mv = helpers.add_cached_version(model, visibility='private')
+ pv = helpers.add_cached_version(protocol, visibility='private')
+ fv = helpers.add_cached_version(fittingspec, visibility='private')
fr = recipes.fittingresult.make(
model=model, model_version=mv,
@@ -254,9 +258,9 @@ def test_viewers_of_public_fittingresult(self, helpers, user):
protocol = recipes.protocol.make()
fittingspec = recipes.fittingspec.make()
dataset = recipes.dataset.make(visibility='public')
- mv = helpers.cached_version(model, visibility='public')
- pv = helpers.cached_version(protocol, visibility='public')
- fv = helpers.cached_version(fittingspec, visibility='public')
+ mv = helpers.add_cached_version(model, visibility='public')
+ pv = helpers.add_cached_version(protocol, visibility='public')
+ fv = helpers.add_cached_version(fittingspec, visibility='public')
fr = recipes.fittingresult.make(
model=model, model_version=mv,
diff --git a/weblab/fitting/tests/test_processing.py b/weblab/fitting/tests/test_processing.py
index b4b6f7928..d562428e5 100644
--- a/weblab/fitting/tests/test_processing.py
+++ b/weblab/fitting/tests/test_processing.py
@@ -41,19 +41,16 @@ def test_creates_new_fittingresult_and_side_effects(
protocol = protocol_with_version
fittingspec = fittingspec_with_version
dataset = public_dataset
- model_version = model.repo.latest_commit.sha
- protocol_version = protocol.repo.latest_commit.sha
- fittingspec_version = fittingspec.repo.latest_commit.sha
+ model_version = model.repocache.latest_version
+ protocol_version = protocol.repocache.latest_version
+ fittingspec_version = fittingspec.repocache.latest_version
assert FittingResult.objects.count() == 0
assert RunningExperiment.objects.count() == 0
version, is_new = submit_fitting(
- model,
model_version,
- protocol,
protocol_version,
- fittingspec,
fittingspec_version,
dataset,
user,
@@ -68,20 +65,20 @@ def test_creates_new_fittingresult_and_side_effects(
assert version.fittingresult.fittingspec == fittingspec
assert version.fittingresult.dataset == dataset
assert version.author == user
- assert version.fittingresult.model_version.sha == model_version
- assert version.fittingresult.protocol_version.sha == protocol_version
- assert version.fittingresult.fittingspec_version.sha == fittingspec_version
+ assert version.fittingresult.model_version == model_version
+ assert version.fittingresult.protocol_version == protocol_version
+ assert version.fittingresult.fittingspec_version == fittingspec_version
assert version.fittingresult.author == user
assert version.status == FittingResultVersion.STATUS_QUEUED
# Check it did submit to the webservice
- model_url = '/entities/models/%d/versions/%s/archive' % (model.pk, model_version)
+ model_url = '/entities/models/%d/versions/%s/archive' % (model.pk, model_version.sha)
protocol_url = (
'/entities/protocols/%d/versions/%s/archive' %
- (protocol.pk, protocol_version))
+ (protocol.pk, protocol_version.sha))
fittingspec_url = (
'/fitting/specs/%d/versions/%s/archive' %
- (fittingspec.pk, fittingspec_version))
+ (fittingspec.pk, fittingspec_version.sha))
dataset_url = '/datasets/%d/archive' % (dataset.pk)
assert mock_post.call_count == 1
@@ -137,12 +134,9 @@ def test_uses_existing_fittingresult(
)
version, is_new = submit_fitting(
- fittingresult.model,
- fittingresult.model_version.sha,
- fittingresult.protocol,
- fittingresult.protocol_version.sha,
- fittingresult.fittingspec,
- fittingresult.fittingspec_version.sha,
+ fittingresult.model_version,
+ fittingresult.protocol_version,
+ fittingresult.fittingspec_version,
fittingresult.dataset,
user,
False,
@@ -166,9 +160,9 @@ def test_raises_exception_on_webservice_error(
mock_post.side_effect = generate_response('something %s')
with pytest.raises(ProcessingException):
submit_fitting(
- model, model_version.sha,
- protocol, protocol_version.sha,
- fittingspec, fittingspec_version.sha,
+ model_version,
+ protocol_version,
+ fittingspec_version,
dataset,
user,
False
@@ -201,9 +195,9 @@ def test_records_submission_error(
mock_post.side_effect = generate_response('%s an error occurred')
version, is_new = submit_fitting(
- model, model_version.sha,
- protocol, protocol_version.sha,
- fittingspec, fittingspec_version.sha,
+ model_version,
+ protocol_version,
+ fittingspec_version,
dataset,
user,
False
@@ -229,9 +223,9 @@ def test_records_inapplicable_result(
mock_post.side_effect = generate_response('%s inapplicable')
version, is_new = submit_fitting(
- model, model_version.sha,
- protocol, protocol_version.sha,
- fittingspec, fittingspec_version.sha,
+ model_version,
+ protocol_version,
+ fittingspec_version,
dataset,
user,
False
diff --git a/weblab/fitting/tests/test_views.py b/weblab/fitting/tests/test_views.py
index f7b633b42..5c6969452 100644
--- a/weblab/fitting/tests/test_views.py
+++ b/weblab/fitting/tests/test_views.py
@@ -3,6 +3,7 @@
import zipfile
from io import BytesIO
from pathlib import Path
+from unittest.mock import patch
import pytest
from django.core.urlresolvers import reverse
@@ -426,6 +427,349 @@ def test_empty_fittingresult_list(self, client, fittingresult_version):
assert len(data['getEntityInfos']['entities']) == 0
+@pytest.mark.django_db
+class TestCreateFittingResultView:
+ def test_requires_login(self, client):
+ response = client.get('/fitting/results/new')
+ assert response.status_code == 302
+
+ def test_requires_permission(self, client, logged_in_user):
+ response = client.get('/fitting/results/new')
+ assert response.status_code == 302
+
+ def test_basic_page(self, client, fits_user):
+ response = client.get('/fitting/results/new')
+ assert response.status_code == 200
+ assert 'form' in response.context
+
+ def test_with_preselected_model(self, client, fits_user, public_model):
+ response = client.get('/fitting/results/new', {'model': public_model.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['model'] == public_model
+
+ def test_with_preselected_protocol(self, client, fits_user, public_protocol):
+ response = client.get('/fitting/results/new', {'protocol': public_protocol.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['protocol'] == public_protocol
+
+ def test_with_preselected_fittingspec(self, client, fits_user, public_fittingspec):
+ response = client.get('/fitting/results/new', {'fittingspec': public_fittingspec.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['fittingspec'] == public_fittingspec
+
+ def test_with_preselected_dataset(self, client, fits_user, public_dataset):
+ response = client.get('/fitting/results/new', {'dataset': public_dataset.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['dataset'] == public_dataset
+
+ def test_with_preselected_model_version(self, client, fits_user, public_model):
+ version = public_model.repocache.latest_version
+ response = client.get('/fitting/results/new', {'model_version': version.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['model'] == public_model
+ assert response.context['form'].initial['model_version'] == version
+
+ def test_with_preselected_protocol_version(self, client, fits_user, public_protocol):
+ version = public_protocol.repocache.latest_version
+ response = client.get('/fitting/results/new', {'protocol_version': version.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['protocol'] == public_protocol
+ assert response.context['form'].initial['protocol_version'] == version
+
+ def test_with_preselected_fittingspec_version(self, client, fits_user, public_fittingspec):
+ version = public_fittingspec.repocache.latest_version
+ response = client.get('/fitting/results/new', {'fittingspec_version': version.pk})
+ assert response.status_code == 200
+ assert response.context['form'].initial['fittingspec'] == public_fittingspec
+ assert response.context['form'].initial['fittingspec_version'] == version
+
+ def test_with_non_visible_model(self, client, fits_user, private_model):
+ response = client.get('/fitting/results/new', {'model': private_model.pk})
+ assert response.status_code == 404
+
+ def test_with_non_visible_model_version(self, client, fits_user, private_model):
+ version = private_model.repocache.latest_version
+ response = client.get('/fitting/results/new', {'model_version': version.pk})
+ assert response.status_code == 404
+
+ def test_with_non_visible_protocol(self, client, fits_user, private_protocol):
+ response = client.get('/fitting/results/new', {'protocol': private_protocol.pk})
+ assert response.status_code == 404
+
+ def test_with_non_visible_protocol_version(self, client, fits_user, private_protocol):
+ version = private_protocol.repocache.latest_version
+ response = client.get('/fitting/results/new', {'protocol_version': version.pk})
+ assert response.status_code == 404
+
+ def test_with_non_visible_fittingspec(self, client, fits_user, private_fittingspec):
+ response = client.get('/fitting/results/new', {'fittingspec': private_fittingspec.pk})
+ assert response.status_code == 404
+
+ def test_with_non_visible_fittingspec_version(self, client, fits_user, private_fittingspec):
+ version = private_fittingspec.repocache.latest_version
+ response = client.get('/fitting/results/new', {'fittingspec_version': version.pk})
+ assert response.status_code == 404
+
+ def test_with_non_visible_dataset(self, client, fits_user, private_dataset):
+ response = client.get('/fitting/results/new', {'dataset': private_dataset.pk})
+ assert response.status_code == 404
+
+ @patch('fitting.views.submit_fitting')
+ def test_submits_to_backend(self, mock_submit, client, fits_user, public_model, public_protocol,
+ public_fittingspec, public_dataset, helpers):
+ model_version = public_model.repocache.latest_version
+ protocol_version = public_protocol.repocache.latest_version
+ fittingspec_version = public_fittingspec.repocache.latest_version
+ helpers.link_to_protocol(public_protocol, public_fittingspec, public_dataset)
+
+ runnable = recipes.fittingresult_version.make()
+ mock_submit.return_value = (runnable, False)
+
+ response = client.post('/fitting/results/new', {
+ 'model': public_model.pk,
+ 'model_version': model_version.pk,
+ 'protocol': public_protocol.pk,
+ 'protocol_version': protocol_version.pk,
+ 'fittingspec': public_fittingspec.pk,
+ 'fittingspec_version': fittingspec_version.pk,
+ 'dataset': public_dataset.pk,
+ })
+
+ assert response.status_code == 302
+ mock_submit.assert_called_with(
+ model_version,
+ protocol_version,
+ fittingspec_version,
+ public_dataset, fits_user, True
+ )
+
+ assert response.url == '/fitting/results/%d/versions/%d' % (runnable.fittingresult.pk, runnable.pk)
+
+
+@pytest.mark.django_db
+class TestFittingResultFilterJsonView:
+ def test_requires_login(self, client):
+ response = client.get('/fitting/results/new/filter')
+ assert response.status_code == 302
+
+ def test_requires_permission(self, client, logged_in_user):
+ response = client.get('/fitting/results/new/filter')
+ assert response.status_code == 302
+
+ def test_all_models_and_versions(self, client, fits_user, helpers):
+ model1 = recipes.model.make()
+ model2 = recipes.model.make()
+ m1v1 = helpers.add_cached_version(model1, visibility='public')
+ m2v1 = helpers.add_cached_version(model2, visibility='public')
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert 'fittingResultOptions' in data
+ options = data['fittingResultOptions']
+ assert set(options['models']) == {model1.id, model2.id}
+ assert set(options['model_versions']) == {m1v1.id, m2v1.id}
+
+ def test_model_and_version_must_be_visible_to_user(self, client, fits_user, helpers):
+ model1 = recipes.model.make()
+ model2 = recipes.model.make()
+ m1v1 = helpers.add_cached_version(model1, visibility='public')
+ m2v1 = helpers.add_cached_version(model2, visibility='private') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert 'fittingResultOptions' in data
+ options = data['fittingResultOptions']
+ assert set(options['models']) == {model1.id}
+ assert set(options['model_versions']) == {m1v1.id}
+
+ def test_all_protocols_and_versions(self, client, fits_user, helpers):
+ protocol1 = recipes.protocol.make()
+ protocol2 = recipes.protocol.make()
+ p1v1 = helpers.add_cached_version(protocol1, visibility='public')
+ p2v1 = helpers.add_cached_version(protocol2, visibility='public')
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert set(options['protocols']) == {protocol1.id, protocol2.id}
+ assert set(options['protocol_versions']) == {p1v1.id, p2v1.id}
+
+ def test_protocol_and_version_must_be_visible_to_user(self, client, fits_user, helpers):
+ protocol1 = recipes.protocol.make()
+ protocol2 = recipes.protocol.make()
+ p1v1 = helpers.add_cached_version(protocol1, visibility='public')
+ p2v1 = helpers.add_cached_version(protocol2, visibility='private') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert 'fittingResultOptions' in data
+ options = data['fittingResultOptions']
+ assert set(options['protocols']) == {protocol1.id}
+ assert set(options['protocol_versions']) == {p1v1.id}
+
+ def test_all_fittingspecs_and_versions(self, client, fits_user, helpers):
+ fittingspec1 = recipes.fittingspec.make()
+ fittingspec2 = recipes.fittingspec.make()
+ f1v1 = helpers.add_cached_version(fittingspec1, visibility='public')
+ f2v1 = helpers.add_cached_version(fittingspec2, visibility='public')
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert set(options['fittingspecs']) == {fittingspec1.id, fittingspec2.id}
+ assert set(options['fittingspec_versions']) == {f1v1.id, f2v1.id}
+
+ def test_fittingspec_and_version_must_be_visible_to_user(self, client, fits_user, helpers):
+ fittingspec1 = recipes.fittingspec.make()
+ fittingspec2 = recipes.fittingspec.make()
+ p1v1 = helpers.add_cached_version(fittingspec1, visibility='public')
+ p2v1 = helpers.add_cached_version(fittingspec2, visibility='private') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert 'fittingResultOptions' in data
+ options = data['fittingResultOptions']
+ assert set(options['fittingspecs']) == {fittingspec1.id}
+ assert set(options['fittingspec_versions']) == {p1v1.id}
+
+ def test_all_datasets(self, client, fits_user):
+ dataset1 = recipes.dataset.make(visibility='public')
+ dataset2 = recipes.dataset.make(visibility='public')
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert set(options['datasets']) == {dataset1.id, dataset2.id}
+
+ def test_dataset_must_be_visible_to_user(self, client, fits_user, helpers):
+ dataset1 = recipes.dataset.make(visibility='public')
+ dataset2 = recipes.dataset.make(visibility='private') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert 'fittingResultOptions' in data
+ options = data['fittingResultOptions']
+ assert set(options['datasets']) == {dataset1.id}
+
+ def test_versions_restricted_when_model_selected(self, client, fits_user, helpers):
+ model1 = recipes.model.make()
+ model2 = recipes.model.make()
+ m1v1 = helpers.add_cached_version(model1, visibility='public')
+ m2v1 = helpers.add_cached_version(model2, visibility='public') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {'model': model1.id})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert options['models'] == [model1.id, model2.id]
+ assert options['model_versions'] == [m1v1.id]
+
+ def test_versions_restricted_when_protocol_selected(self, client, fits_user, helpers):
+ protocol1 = recipes.protocol.make()
+ protocol2 = recipes.protocol.make()
+ p1v1 = helpers.add_cached_version(protocol1, visibility='public')
+ p2v1 = helpers.add_cached_version(protocol2, visibility='public') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {'protocol': protocol1.id})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert options['protocols'] == [protocol1.id, protocol2.id]
+ assert options['protocol_versions'] == [p1v1.id]
+
+ def test_versions_restricted_when_fittingspec_selected(self, client, fits_user, helpers):
+ fittingspec1 = recipes.fittingspec.make()
+ fittingspec2 = recipes.fittingspec.make()
+ f1v1 = helpers.add_cached_version(fittingspec1, visibility='public')
+ f2v1 = helpers.add_cached_version(fittingspec2, visibility='public') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {'fittingspec': fittingspec1.id})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert options['fittingspecs'] == [fittingspec1.id, fittingspec2.id]
+ assert options['fittingspec_versions'] == [f1v1.id]
+
+ def test_dataset_and_fittingspec_restricted_when_protocol_selected(self, client, fits_user, helpers):
+ protocol = recipes.protocol.make()
+ helpers.add_cached_version(protocol, visibility='public')
+ fittingspec1 = recipes.fittingspec.make(protocol=protocol)
+ fittingspec2 = recipes.fittingspec.make()
+ f1v1 = helpers.add_cached_version(fittingspec1, visibility='public')
+ f2v1 = helpers.add_cached_version(fittingspec2, visibility='public') # noqa: F841
+ dataset1 = recipes.dataset.make(protocol=protocol, visibility='public')
+ dataset2 = recipes.dataset.make(visibility='public') # noqa: F841
+
+ response = client.get('/fitting/results/new/filter', {'protocol': protocol.id})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert options['datasets'] == [dataset1.id]
+ assert options['fittingspecs'] == [fittingspec1.id]
+ assert options['fittingspec_versions'] == [f1v1.id]
+
+ def test_protocol_and_fittingspec_restricted_when_dataset_selected(self, client, fits_user, helpers):
+ protocol1 = recipes.protocol.make()
+ protocol2 = recipes.protocol.make()
+ p1v1 = helpers.add_cached_version(protocol1, visibility='public')
+ p2v1 = helpers.add_cached_version(protocol2, visibility='public') # noqa: F841
+ fittingspec1 = recipes.fittingspec.make(protocol=protocol1)
+ fittingspec2 = recipes.fittingspec.make()
+ f1v1 = helpers.add_cached_version(fittingspec1, visibility='public')
+ f2v1 = helpers.add_cached_version(fittingspec2, visibility='public') # noqa: F841
+
+ dataset = recipes.dataset.make(protocol=protocol1, visibility='public')
+
+ response = client.get('/fitting/results/new/filter', {'dataset': dataset.id})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert options['protocols'] == [protocol1.id]
+ assert options['protocol_versions'] == [p1v1.id]
+ assert options['fittingspecs'] == [fittingspec1.id]
+ assert options['fittingspec_versions'] == [f1v1.id]
+
+ def test_protocol_and_dataset_restricted_when_fittingspec_selected(self, client, fits_user, helpers):
+ protocol1 = recipes.protocol.make()
+ protocol2 = recipes.protocol.make()
+ p1v1 = helpers.add_cached_version(protocol1, visibility='public')
+ p2v1 = helpers.add_cached_version(protocol2, visibility='public') # noqa: F841
+ dataset1 = recipes.dataset.make(protocol=protocol1, visibility='public')
+ dataset2 = recipes.dataset.make(visibility='public') # noqa: F841
+
+ fittingspec = recipes.fittingspec.make(protocol=protocol1)
+
+ response = client.get('/fitting/results/new/filter', {'fittingspec': fittingspec.id})
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ options = data['fittingResultOptions']
+ assert options['protocols'] == [protocol1.id]
+ assert options['protocol_versions'] == [p1v1.id]
+ assert options['datasets'] == [dataset1.id]
+
+
@pytest.mark.django_db
class TestFittingSpecRenaming:
def test_fittingspec_renaming_success(self, client, logged_in_user, helpers):
@@ -474,3 +818,106 @@ def test_dataset_renaming_same_users_fails(self, client, logged_in_user, helpers
assert response.status_code == 200
fittingspec = FittingSpec.objects.first()
assert fittingspec.name == 'my spec1'
+
+
+@pytest.mark.django_db
+class TestRerunFittingView:
+ def test_requires_login(self, client):
+ response = client.post('/fitting/results/rerun')
+ assert response.status_code == 200
+ data = json.loads(response.content.decode())
+ assert not data['newExperiment']['response']
+ assert (
+ data['newExperiment']['responseText'] ==
+ 'You are not allowed to run fitting experiments'
+ )
+
+ def test_requires_permission(self, client, logged_in_user):
+ response = client.post('/fitting/results/rerun')
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert not data['newExperiment']['response']
+ assert (
+ data['newExperiment']['responseText'] ==
+ 'You are not allowed to run fitting experiments'
+ )
+
+ def test_raises_error_if_no_rerun_id(self, client, fits_user):
+ response = client.post('/fitting/results/rerun')
+ assert response.status_code == 200
+
+ data = json.loads(response.content.decode())
+ assert not data['newExperiment']['response']
+ assert (
+ data['newExperiment']['responseText'] ==
+ 'You must specify a fitting experiment to rerun'
+ )
+
+ @patch('fitting.views.submit_fitting')
+ def test_rerun_experiment(
+ self, mock_submit, client, fits_user, fittingresult_version
+ ):
+
+ fittingresult = fittingresult_version.fittingresult
+ new_runnable = recipes.fittingresult_version.make(fittingresult=fittingresult)
+ mock_submit.return_value = (new_runnable, True)
+
+ response = client.post(
+ '/fitting/results/rerun',
+ {
+ 'rerun': fittingresult_version.pk,
+ }
+ )
+
+ assert response.status_code == 200
+ data = json.loads(response.content.decode())
+ url = '/fitting/results/%d/versions/%d' % (fittingresult.id, new_runnable.id)
+
+ assert 'newExperiment' in data
+ assert data['newExperiment']['expId'] == fittingresult.id
+ assert data['newExperiment']['versionId'] == new_runnable.id
+ assert data['newExperiment']['url'] == url
+ assert data['newExperiment']['expName'] == fittingresult.name
+ assert data['newExperiment']['status'] == 'QUEUED'
+ assert data['newExperiment']['response'] is True
+ message = data['newExperiment']['responseText']
+ assert url in message
+ assert fittingresult.name in message
+ assert 'submitted to the queue' in message
+ assert 'Experiment' in message
+
+ @patch('fitting.views.submit_fitting')
+ def test_rerun_experiment_with_failure(
+ self, mock_submit, client, fits_user, fittingresult_version
+ ):
+
+ fittingresult = fittingresult_version.fittingresult
+ new_runnable = recipes.fittingresult_version.make(
+ fittingresult=fittingresult, status='FAILED', return_text='something failed'
+ )
+ mock_submit.return_value = (new_runnable, True)
+
+ response = client.post(
+ '/fitting/results/rerun',
+ {
+ 'rerun': fittingresult_version.pk,
+ }
+ )
+
+ assert response.status_code == 200
+ data = json.loads(response.content.decode())
+ url = '/fitting/results/%d/versions/%d' % (fittingresult.id, new_runnable.id)
+
+ assert 'newExperiment' in data
+ assert data['newExperiment']['expId'] == fittingresult.id
+ assert data['newExperiment']['versionId'] == new_runnable.id
+ assert data['newExperiment']['url'] == url
+ assert data['newExperiment']['expName'] == fittingresult.name
+ assert data['newExperiment']['status'] == 'FAILED'
+ assert data['newExperiment']['response'] is False
+ message = data['newExperiment']['responseText']
+ assert url in message
+ assert fittingresult.name in message
+ assert 'could not be run: something failed' in message
+ assert 'Experiment' in message
diff --git a/weblab/fitting/urls.py b/weblab/fitting/urls.py
index 9c9a17118..c8b3c9733 100644
--- a/weblab/fitting/urls.py
+++ b/weblab/fitting/urls.py
@@ -66,6 +66,24 @@
views.FittingResultComparisonJsonView.as_view(),
name='compare_json',
),
+
+ url(
+ r'^new$',
+ views.FittingResultCreateView.as_view(),
+ name='new'
+ ),
+
+ url(
+ r'^new/filter$',
+ views.FittingResultFilterJsonView.as_view(),
+ name='filter_json',
+ ),
+
+ url(
+ r'^rerun$',
+ views.FittingResultRerunView.as_view(),
+ name='rerun'
+ ),
]
urlpatterns = [
diff --git a/weblab/fitting/views.py b/weblab/fitting/views.py
index 78f156427..a7611ace1 100644
--- a/weblab/fitting/views.py
+++ b/weblab/fitting/views.py
@@ -13,20 +13,31 @@
from braces.views import UserFormKwargsMixin
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
+from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
+from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.text import get_valid_filename
from django.views import View
from django.views.generic import TemplateView
from django.views.generic.detail import DetailView, SingleObjectMixin
-from django.views.generic.edit import CreateView
+from django.views.generic.edit import CreateView, FormView
from core.visibility import VisibilityMixin
from datasets import views as dataset_views
+from datasets.models import Dataset
+from entities.models import ModelEntity, ProtocolEntity
from entities.views import EntityNewVersionView, EntityTypeMixin, RenameView
+from repocache.models import CachedFittingSpecVersion, CachedModelVersion, CachedProtocolVersion
-from .forms import FittingSpecForm, FittingSpecRenameForm, FittingSpecVersionForm
-from .models import FittingResult, FittingResultVersion
+from .forms import (
+ FittingResultCreateForm,
+ FittingSpecForm,
+ FittingSpecRenameForm,
+ FittingSpecVersionForm,
+)
+from .models import FittingResult, FittingResultVersion, FittingSpec
+from .processing import submit_fitting
class FittingSpecCreateView(
@@ -64,6 +75,7 @@ class FittingResultVersionListView(VisibilityMixin, DetailView):
class FittingResultVersionView(VisibilityMixin, DetailView):
+ """Show a version of a fitting result"""
model = FittingResultVersion
context_object_name = 'version'
@@ -202,3 +214,224 @@ def get(self, request, *args, **kwargs):
}
return JsonResponse(response)
+
+
+class FittingResultCreateView(LoginRequiredMixin, PermissionRequiredMixin, UserFormKwargsMixin, FormView):
+ """
+ Create and submit a fitting result from models, protocols, fitting specs and datasets
+ and (where relevant) their versions.
+ """
+ permission_required = 'fitting.run_fits'
+ form_class = FittingResultCreateForm
+
+ template_name = 'fitting/fittingresult_create_form.html'
+
+ def get_initial(self):
+ initial = super().get_initial()
+ model_id = self.request.GET.get('model')
+ model_version_id = self.request.GET.get('model_version')
+ protocol_id = self.request.GET.get('protocol')
+ protocol_version_id = self.request.GET.get('protocol_version')
+ fittingspec_id = self.request.GET.get('fittingspec')
+ fittingspec_version_id = self.request.GET.get('fittingspec_version')
+ dataset_id = self.request.GET.get('dataset')
+
+ if model_version_id:
+ initial['model_version'] = get_object_or_404(
+ CachedModelVersion.objects.visible_to_user(self.request.user),
+ pk=model_version_id)
+ initial['model'] = initial['model_version'].model
+ elif model_id:
+ initial['model'] = get_object_or_404(
+ ModelEntity.objects.visible_to_user(self.request.user),
+ pk=model_id)
+
+ if protocol_version_id:
+ initial['protocol_version'] = get_object_or_404(
+ CachedProtocolVersion.objects.visible_to_user(self.request.user),
+ pk=protocol_version_id)
+ initial['protocol'] = initial['protocol_version'].protocol
+ elif protocol_id:
+ initial['protocol'] = get_object_or_404(
+ ProtocolEntity.objects.visible_to_user(self.request.user),
+ pk=protocol_id)
+
+ if fittingspec_version_id:
+ initial['fittingspec_version'] = get_object_or_404(
+ CachedFittingSpecVersion.objects.visible_to_user(self.request.user),
+ pk=fittingspec_version_id)
+ initial['fittingspec'] = initial['fittingspec_version'].fittingspec
+ elif fittingspec_id:
+ initial['fittingspec'] = get_object_or_404(
+ FittingSpec.objects.visible_to_user(self.request.user),
+ pk=fittingspec_id)
+
+ if dataset_id:
+ initial['dataset'] = get_object_or_404(
+ Dataset.objects.visible_to_user(self.request.user),
+ pk=dataset_id)
+
+ return initial
+
+ def form_valid(self, form):
+ self.runnable, is_new = submit_fitting(
+ form.cleaned_data['model_version'],
+ form.cleaned_data['protocol_version'],
+ form.cleaned_data['fittingspec_version'],
+ form.cleaned_data['dataset'],
+ self.request.user,
+ True,
+ )
+
+ queued = self.runnable.status == FittingResultVersion.STATUS_QUEUED
+
+ if is_new:
+ if queued:
+ messages.info(self.request, "Fitting experiment submitted to the queue.")
+ else:
+ messages.error(self.request, "Fitting experiment could not be run: " + self.runnable.return_text)
+ else:
+ messages.info(self.request, "Fitting experiment was already run.")
+
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse('fitting:result:version', args=[self.runnable.fittingresult.pk, self.runnable.pk])
+
+
+class FittingResultFilterJsonView(LoginRequiredMixin, PermissionRequiredMixin, View):
+ """
+ JSON view of valid fitting result input values based on those already selected
+
+ For example, if a model id is specified (as a GET param), only versions of that model
+ (which are visible to the user) will be included in the results. Otherwise all visible
+ models and versions will be in the results (which are simply a list of database
+ IDs of the relevant objects)
+
+ Connections between protocols, fitting specs and datasets are also enforced.
+ """
+ permission_required = 'fitting.run_fits'
+
+ def get(self, request, *args, **kwargs):
+ options = {}
+
+ model_versions = CachedModelVersion.objects.visible_to_user(request.user)
+ models = ModelEntity.objects.filter(cachedmodel__versions__in=model_versions)
+ protocol_versions = CachedProtocolVersion.objects.visible_to_user(request.user)
+ protocols = ProtocolEntity.objects.filter(cachedprotocol__versions__in=protocol_versions)
+ fittingspec_versions = CachedFittingSpecVersion.objects.visible_to_user(request.user)
+ fittingspecs = FittingSpec.objects.filter(cachedfittingspec__versions__in=fittingspec_versions)
+ datasets = Dataset.objects.visible_to_user(request.user)
+
+ def _get_int_param(fieldname, _model):
+ try:
+ pk = int(request.GET.get(fieldname, ''))
+ return _model.objects.get(pk=pk)
+ except ValueError:
+ pass
+ except ObjectDoesNotExist:
+ pass
+
+ model = _get_int_param('model', ModelEntity)
+ protocol = _get_int_param('protocol', ProtocolEntity)
+ fittingspec = _get_int_param('fittingspec', FittingSpec)
+ dataset = _get_int_param('dataset', Dataset)
+
+ if not protocol:
+ if fittingspec:
+ protocol = fittingspec.protocol
+ protocols = [protocol]
+ elif dataset:
+ protocol = dataset.protocol
+ protocols = [protocol]
+
+ # Restrict to versions of specified model
+ if model:
+ model_versions = model_versions.filter(entity__entity=model)
+
+ # Restrict to versions of specified fitting spec
+ if fittingspec:
+ fittingspec_versions = fittingspec_versions.filter(entity__entity=fittingspec)
+
+ # Restrict to versions of specified protocol
+ if protocol:
+ protocol_versions = protocol_versions.filter(entity__entity=protocol)
+
+ # If no fitting spec was chosen yet, restrict to those linked to this protocol
+ if not fittingspec:
+ fittingspecs = fittingspecs.filter(protocol=protocol.id)
+ fsids = [fs.pk for fs in fittingspecs.filter(protocol=protocol.id)]
+ fittingspec_versions = fittingspec_versions.filter(entity__entity__in=fsids)
+
+ # If no dataset was chosen yet, restrict to those linked to this protocol
+ if not dataset:
+ datasets = datasets.filter(protocol=protocol.id)
+
+ # These might be either querysets or ID lists
+ def _get_ids(qs):
+ return [item.id for item in qs]
+
+ options['models'] = _get_ids(models)
+ options['model_versions'] = _get_ids(model_versions)
+ options['protocols'] = _get_ids(protocols)
+ options['protocol_versions'] = _get_ids(protocol_versions)
+ options['fittingspecs'] = _get_ids(fittingspecs)
+ options['fittingspec_versions'] = _get_ids(fittingspec_versions)
+ options['datasets'] = _get_ids(datasets)
+
+ return JsonResponse({
+ 'fittingResultOptions': options
+ })
+
+
+class FittingResultRerunView(PermissionRequiredMixin, View):
+ permission_required = 'fitting.run_fits'
+
+ def handle_no_permission(self):
+ return JsonResponse({
+ 'newExperiment': {
+ 'response': False,
+ 'responseText': 'You are not allowed to run fitting experiments',
+ }
+ })
+
+ def post(self, request, *args, **kwargs):
+ if 'rerun' in request.POST:
+ version = get_object_or_404(FittingResultVersion, pk=request.POST['rerun'])
+
+ version, is_new = submit_fitting(
+ version.fittingresult.model_version,
+ version.fittingresult.protocol_version,
+ version.fittingresult.fittingspec_version,
+ version.fittingresult.dataset,
+ request.user,
+ rerun_ok=True
+ )
+
+ queued = version.status == FittingResultVersion.STATUS_QUEUED
+ version_url = reverse('fitting:result:version', args=[version.fittingresult.id, version.id])
+ if queued:
+ msg = " submitted to the queue."
+ else:
+ msg = " could not be run: " + version.return_text
+
+ return JsonResponse({
+ 'newExperiment': {
+ 'expId': version.fittingresult.id,
+ 'versionId': version.id,
+ 'url': version_url,
+ 'expName': version.fittingresult.name,
+ 'status': version.status,
+ 'response': (not is_new) or queued,
+ 'responseText': "Experiment {} {}".format(
+ version_url, version.fittingresult.name, msg
+ )
+ }
+ })
+ else:
+ return JsonResponse({
+ 'newExperiment': {
+ 'response': False,
+ 'responseText': 'You must specify a fitting experiment to rerun',
+ }
+ })
diff --git a/weblab/repocache/models.py b/weblab/repocache/models.py
index 6d2ba638b..80475e449 100644
--- a/weblab/repocache/models.py
+++ b/weblab/repocache/models.py
@@ -1,5 +1,6 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
+from guardian.shortcuts import get_objects_for_user
from core.models import VisibilityModelMixin
from core.visibility import Visibility
@@ -202,6 +203,30 @@ def _set_class_links(entity_cache_type, version_cache_type, tag_cache_type):
tag_cache_type.CachedVersionClass = version_cache_type
+class CachedEntityVersionManager(models.Manager):
+ def visible_to_user(self, user):
+ """Query over all cached entity versions that the given user can view.
+
+ This includes those versions of entities of the relevant type for which either:
+ - the user is the author of the related entity
+ - the entity version is non-private
+ - or the entity is explicitly shared with the user
+ """
+ non_private = self.filter(visibility__in=['public', 'moderated'])
+
+ if user.is_authenticated:
+ shared_pks = get_objects_for_user(
+ user, 'entities.edit_entity', with_superuser=False
+ ).values_list('pk', flat=True)
+ shared = self.filter(entity__entity__pk__in=shared_pks)
+ owned = self.filter(entity__entity__author=user)
+ else:
+ shared = self.none()
+ owned = self.none()
+
+ return non_private | owned | shared
+
+
####################################################################################################
#
# Concrete cache classes go here
@@ -216,6 +241,12 @@ class CachedModelVersion(CachedEntityVersion):
"""Cache for a single version / commit in a CellML model's repository."""
entity = models.ForeignKey(CachedModel, on_delete=models.CASCADE, related_name='versions')
+ objects = CachedEntityVersionManager()
+
+ @property
+ def model(self):
+ return self.entity.entity
+
class CachedModelTag(CachedEntityTag):
"""Cache for a tag in a CellML model's repository."""
@@ -235,6 +266,12 @@ class CachedProtocolVersion(CachedEntityVersion):
"""Cache for a single version / commit in a protocol's repository."""
entity = models.ForeignKey(CachedProtocol, on_delete=models.CASCADE, related_name='versions')
+ objects = CachedEntityVersionManager()
+
+ @property
+ def protocol(self):
+ return self.entity.entity
+
class CachedProtocolTag(CachedEntityTag):
"""Cache for a tag in a protocol's repository."""
@@ -254,6 +291,12 @@ class CachedFittingSpecVersion(CachedEntityVersion):
"""Cache for a single version / commit in a fitting specifications's repository."""
entity = models.ForeignKey(CachedFittingSpec, on_delete=models.CASCADE, related_name='versions')
+ objects = CachedEntityVersionManager()
+
+ @property
+ def fittingspec(self):
+ return self.entity.entity
+
class CachedFittingSpecTag(CachedEntityTag):
"""Cache for a tag in a fitting specifications's repository."""
diff --git a/weblab/repocache/tests/test_models.py b/weblab/repocache/tests/test_models.py
index 436391063..f9a635227 100644
--- a/weblab/repocache/tests/test_models.py
+++ b/weblab/repocache/tests/test_models.py
@@ -3,7 +3,12 @@
from core import recipes
from repocache.exceptions import RepoCacheMiss
-from repocache.models import CachedModel, CachedProtocol
+from repocache.models import (
+ CachedFittingSpec,
+ CachedModel,
+ CachedModelVersion,
+ CachedProtocol,
+)
from repocache.populate import populate_entity_cache
@@ -12,6 +17,7 @@ class TestEntityCacheModels:
@pytest.mark.parametrize("recipe,manager", [
(recipes.cached_model, CachedModel.objects),
(recipes.cached_protocol, CachedProtocol.objects),
+ (recipes.cached_fittingspec, CachedFittingSpec.objects),
])
def test_cachedentity_is_deleted_when_entity_is_deleted(self, recipe, manager):
cached = recipe.make()
@@ -21,6 +27,7 @@ def test_cachedentity_is_deleted_when_entity_is_deleted(self, recipe, manager):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_version),
(recipes.cached_protocol_version),
+ (recipes.cached_fittingspec_version),
])
def test_related_names_for_versions(self, recipe):
version = recipe.make()
@@ -29,6 +36,7 @@ def test_related_names_for_versions(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_tag),
(recipes.cached_protocol_tag),
+ (recipes.cached_fittingspec_tag),
])
def test_related_names_for_tags(self, recipe):
tag = recipe.make()
@@ -37,6 +45,7 @@ def test_related_names_for_tags(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_version),
(recipes.cached_protocol_version),
+ (recipes.cached_fittingspec_version),
])
def test_uniqueness_of_entity_and_version_sha(self, recipe):
version = recipe.make()
@@ -46,6 +55,7 @@ def test_uniqueness_of_entity_and_version_sha(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_tag),
(recipes.cached_protocol_tag),
+ (recipes.cached_fittingspec_tag),
])
def test_uniqueness_of_entity_and_tag(self, recipe):
version = recipe.make()
@@ -55,18 +65,29 @@ def test_uniqueness_of_entity_and_tag(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model),
(recipes.cached_protocol),
+ (recipes.cached_fittingspec),
])
def test_uniqueness_of_entity(self, recipe):
cached = recipe.make()
with pytest.raises(IntegrityError):
recipe.make(entity=cached.entity)
+ @pytest.mark.parametrize("recipe,property_name", [
+ (recipes.cached_model_version, 'model'),
+ (recipes.cached_protocol_version, 'protocol'),
+ (recipes.cached_fittingspec_version, 'fittingspec'),
+ ])
+ def test_entity_property(self, recipe, property_name):
+ cached = recipe.make()
+ assert getattr(cached, property_name) == cached.entity.entity
+
@pytest.mark.django_db
class TestEntityCacheModelsVisibility:
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_version),
(recipes.cached_protocol_version),
+ (recipes.cached_fittingspec_version),
])
def test_entity_visibility(self, recipe):
version = recipe.make(visibility='public')
@@ -75,6 +96,7 @@ def test_entity_visibility(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model),
(recipes.cached_protocol),
+ (recipes.cached_fittingspec),
])
def test_entity_visibility_is_private_if_no_versions(self, recipe):
cached = recipe.make()
@@ -83,6 +105,7 @@ def test_entity_visibility_is_private_if_no_versions(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_version),
(recipes.cached_protocol_version),
+ (recipes.cached_fittingspec_version),
])
def test_get_version(self, recipe):
version = recipe.make()
@@ -91,6 +114,7 @@ def test_get_version(self, recipe):
@pytest.mark.parametrize("recipe", [
(recipes.cached_model_version),
(recipes.cached_protocol_version),
+ (recipes.cached_fittingspec_version),
])
def test_set_entity_version_visibility(self, recipe):
version = recipe.make()
@@ -137,3 +161,60 @@ def test_nice_version(self, model_with_version):
model_with_version.repo.tag('v1')
populate_entity_cache(model_with_version)
assert version.nice_version() == 'v1'
+
+
+@pytest.mark.django_db
+class TestCachedEntityVersionVisibleToUser:
+ def test_visibility_and_sharing(self, user, anon_user, other_user, admin_user, helpers):
+ # Own entities -> always visible
+ own_models = recipes.model.make(author=user, _quantity=3)
+ own_moderated_version = helpers.add_fake_version(own_models[0], 'moderated')
+ own_public_version = helpers.add_fake_version(own_models[1], 'public')
+ own_private_version = helpers.add_fake_version(own_models[2], 'private')
+
+ # Other entity type shouldn't show up
+ own_protocol = recipes.protocol.make(author=user)
+ helpers.add_fake_version(own_protocol, 'moderated')
+
+ # Non-shared public/moderated entities -> visible
+ other_public_models = recipes.model.make(author=other_user, _quantity=2)
+ other_moderated_version = helpers.add_fake_version(other_public_models[0], 'moderated')
+ other_public_version = helpers.add_fake_version(other_public_models[1], 'public')
+
+ # Non-shared private entities -> not visible
+ other_private_model = recipes.model.make(author=other_user)
+ other_private_version = helpers.add_fake_version(other_private_model, 'private') # noqa: F841
+
+ # Shared public or private entities -> visible
+ other_shared_model = recipes.model.make(author=other_user)
+ other_shared_version = helpers.add_fake_version(other_shared_model, 'private')
+ other_shared_model.add_collaborator(user)
+
+ other_shared_protocol = recipes.protocol.make(author=other_user)
+ helpers.add_fake_version(other_shared_protocol, 'private')
+ other_shared_protocol.add_collaborator(user)
+
+ # User can see own versions, plus public, plus those explicitly shared
+ visible_to_self = CachedModelVersion.objects.visible_to_user(user).all()
+ assert visible_to_self.count() == 6
+ assert set(visible_to_self) == {
+ own_moderated_version, own_public_version, own_private_version,
+ other_moderated_version, other_public_version,
+ other_shared_version
+ }
+
+# # Anonymous users only see public things
+ visible_to_anon = CachedModelVersion.objects.visible_to_user(anon_user).all()
+ assert visible_to_anon.count() == 4
+ assert set(visible_to_anon) == {
+ own_moderated_version, own_public_version,
+ other_moderated_version, other_public_version,
+ }
+
+ # Admins don't get special visibility rights, so only see public entities
+ visible_to_admin = CachedModelVersion.objects.visible_to_user(admin_user).all()
+ assert visible_to_admin.count() == 4
+ assert set(visible_to_admin) == {
+ own_moderated_version, own_public_version,
+ other_moderated_version, other_public_version,
+ }
diff --git a/weblab/static/js/experiment.js b/weblab/static/js/experiment.js
index 32a3f1f9c..760f69ad8 100644
--- a/weblab/static/js/experiment.js
+++ b/weblab/static/js/experiment.js
@@ -912,13 +912,14 @@ function init() {
var resubmitAction = document.getElementById("rerunExperimentAction");
if (resubmit && resubmitAction)
{
+ var resubmitHref = $(resubmit).data('href');
resubmit.addEventListener("click", function (ev) {
resubmitAction.innerHTML = "
";
var exp_ver = versions[curVersion.id],
jsonObject = {
rerun: exp_ver.id,
};
- $.post('/experiments/new', jsonObject, function(data) {
+ $.post(resubmitHref, jsonObject, function(data) {
var msg = data.newExperiment.responseText;
if (data.newExperiment.response)
{
diff --git a/weblab/static/js/fitting.js b/weblab/static/js/fitting.js
new file mode 100644
index 000000000..dee13f0ca
--- /dev/null
+++ b/weblab/static/js/fitting.js
@@ -0,0 +1,81 @@
+
+function init() {
+ var $model = $("#id_model");
+ var $modelVersion = $("#id_model_version");
+ var $protocol = $("#id_protocol");
+ var $protocolVersion = $("#id_protocol_version");
+ var $fittingSpec = $("#id_fittingspec");
+ var $fittingSpecVersion = $("#id_fittingspec_version");
+ var $dataset = $("#id_dataset");
+
+ function restrictIds(idList, $dropdown) {
+ $dropdown.find("option").each(function(i, opt) {
+ if (opt.value.length > 0) {
+ $(opt).toggle(
+ idList.includes(parseInt(opt.value, 10))
+ );
+ }
+ });
+ }
+
+ function updateDropdowns() {
+ var modelId = parseInt($model.val(), 10);
+ var protocolId = parseInt($protocol.val(), 10);
+ var fittingSpecId = parseInt($fittingSpec.val(), 10);
+ var datasetId = parseInt($dataset.val(), 10);
+
+ console.log(modelId, protocolId, fittingSpecId, datasetId);
+
+ // Clear selected version if no entity selected
+ if (isNaN(modelId)) $modelVersion.val('');
+ if (isNaN(protocolId)) $protocolVersion.val('');
+ if (isNaN(fittingSpecId)) $fittingSpecVersion.val('');
+
+ // Disable version dropdowns if entity not selected
+ $modelVersion.prop('disabled', isNaN(modelId));
+ $protocolVersion.prop('disabled', isNaN(protocolId));
+ $fittingSpecVersion.prop('disabled', isNaN(fittingSpecId));
+
+ $.getJSON("/fitting/results/new/filter",
+ {
+ 'model': modelId,
+ 'protocol': protocolId,
+ 'fittingspec': fittingSpecId,
+ 'dataset': datasetId,
+ },
+ function(json) {
+ if (json.fittingResultOptions) {
+ var results = json.fittingResultOptions;
+
+ restrictIds(results.models, $model);
+ restrictIds(results.model_versions, $modelVersion);
+ restrictIds(results.protocols, $protocol);
+ restrictIds(results.protocol_versions, $protocolVersion);
+ restrictIds(results.fittingspecs, $fittingSpec);
+ restrictIds(results.fittingspec_versions, $fittingSpecVersion);
+ restrictIds(results.datasets, $dataset);
+ }
+ }
+ );
+ }
+
+ $('#resetbutton').click(function() {
+ $('form select:enabled').val('');
+ });
+
+ $("form select").change(updateDropdowns);
+
+ // Disabled form fields will not be submitted - re-enable before the form is posted
+ $('form').submit(function() {
+ $(':disabled').each(function() {
+ $(this).removeAttr('disabled');
+ })
+ });
+
+ updateDropdowns();
+}
+
+
+module.exports = {
+ init: init,
+};
diff --git a/weblab/static/js/main.js b/weblab/static/js/main.js
index 96af8a9a8..a1b860c0e 100644
--- a/weblab/static/js/main.js
+++ b/weblab/static/js/main.js
@@ -8,6 +8,7 @@ require('./db.js');
var entity = require('./entity.js');
var experiment = require('./experiment.js');
var notifications = require('./lib/notifications.js');
+var fitting = require('./fitting.js');
require('./compare.js');
require('./entity_version_list.js');
require('./experiment_tasks.js');
@@ -168,6 +169,10 @@ function initPage ()
addText: 'add another',
deleteText: 'remove',
});
+
+ if ($('#fitting-result-submit').length > 0) {
+ fitting.init();
+ }
}
document.addEventListener("DOMContentLoaded", initPage, false);
diff --git a/weblab/templates/datasets/dataset_detail.html b/weblab/templates/datasets/dataset_detail.html
index 09aa4ee7c..2743eb442 100644
--- a/weblab/templates/datasets/dataset_detail.html
+++ b/weblab/templates/datasets/dataset_detail.html
@@ -38,6 +38,9 @@
+
{{ dataset.description }} diff --git a/weblab/templates/entities/entity_version.html b/weblab/templates/entities/entity_version.html index 003b19ac5..c110ea4cb 100644 --- a/weblab/templates/entities/entity_version.html +++ b/weblab/templates/entities/entity_version.html @@ -63,10 +63,14 @@
+
{% endif %}
{% can_delete_entity version as can_delete %}
diff --git a/weblab/templates/fitting/fittingresult_create_form.html b/weblab/templates/fitting/fittingresult_create_form.html
new file mode 100644
index 000000000..8810aae65
--- /dev/null
+++ b/weblab/templates/fitting/fittingresult_create_form.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+
+{% block title %}Submit fitting experiment - {% endblock title %}
+
+{% block body_id %}fitting-result-submit{% endblock body_id %}
+
+{% block content %}
+
++To run a fitting experiment, you need to select not just the model to fit and the data to fit to, +but also the Web Lab protocol describing how to simulate the data from the model, +and a "fitting specification" detailing the fitting algorithm to use, its settings, +which model parameters should be fit, and how to compare the simulation results to data. +
+ ++Available options will be filtered according to the interfaces they declare, +so only "compatible" entities may be used together. +
+ + + +{% endblock content %} diff --git a/weblab/templates/fitting/fittingresultversion_detail.html b/weblab/templates/fitting/fittingresultversion_detail.html index 4f1bf2cc9..0bb7eca83 100644 --- a/weblab/templates/fitting/fittingresultversion_detail.html +++ b/weblab/templates/fitting/fittingresultversion_detail.html @@ -33,7 +33,7 @@
+
{% endif %}
{% can_delete_entity version as can_delete %}
@@ -48,6 +48,8 @@