Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cea4975
Set up basic fitting experiment form
helenst Sep 14, 2020
b45e0b5
Fitting result form: validate object combinations
helenst Sep 14, 2020
3dbffc5
Accept preselected entity in Fitting Result form
helenst Sep 14, 2020
ebc17c7
Fitting result form submit to back end
helenst Sep 15, 2020
9b5b15b
Change signature of submit_fitting
helenst Sep 15, 2020
4927380
Change submit_experiment call signature
helenst Sep 15, 2020
1e6dc4d
Add messages to fitting experiment submission
helenst Sep 15, 2020
1fa7533
Allow reruns of fitting experiments
helenst Sep 15, 2020
15c3fa9
Add basic javascript for fitting result form
helenst Sep 15, 2020
219c29b
Add json endpoint for filtering fitting form
helenst Sep 17, 2020
9fbd961
Apply restrictions to fitting form dropdowns
helenst Sep 18, 2020
43f2f02
Enforce visibility on fitting form
helenst Sep 21, 2020
7760b20
Enforce visibility on fitting filter json view
helenst Sep 21, 2020
079be8a
Disable pre-selected entity fields
helenst Sep 22, 2020
0fa8417
Display "Fit" buttons on entity pages
helenst Sep 22, 2020
23ff8b4
Add fit button to dataset page
helenst Sep 22, 2020
8e69767
Rename cached_version to add_cached_version
helenst Sep 24, 2020
31ffb66
Add more comments
helenst Sep 24, 2020
951ab2c
Check visibility on fitting result form page load
helenst Sep 24, 2020
90bf332
Support fitting experiments in access token checks
jonc125 Sep 24, 2020
1dbd14c
Remove unnecessary code
helenst Sep 24, 2020
c07b2b0
Query fixes on fitting json view
helenst Sep 25, 2020
dfc8c14
Use nice (tag-based) labels for version dropdowns
helenst Sep 25, 2020
aad1cb1
Allow version to be specified on fitting form load
helenst Sep 25, 2020
6874283
Fitting form: allow specifying multiple entity types
helenst Sep 25, 2020
c6f46dd
Pass entity version id to fitting form
helenst Sep 25, 2020
2003eee
Add test cases for download with access token
helenst Sep 25, 2020
fe4bbfd
Fix querying of entities on experiments
helenst Sep 29, 2020
48b6724
Display fitting spec name on result page
helenst Sep 29, 2020
fc7f0b2
Add more description to form and tidy labels
jonc125 Oct 2, 2020
c7c1ed6
Merge branch 'master' into 277-run-fitting-experiment
jonc125 Oct 2, 2020
cff4c7a
Enable resubmission of fitting experiment
helenst Oct 5, 2020
67fd2d9
Fix sha reference
helenst Oct 5, 2020
c017416
Formatting fixes
helenst Oct 5, 2020
dfab9c4
Don't over restrict fitting dropdowns
helenst Oct 5, 2020
391a14b
Add reset button to fitting form
helenst Oct 5, 2020
0ee83f9
Remove duplicate fixture
helenst Oct 6, 2020
67c764a
Remove unused imports
jonc125 Oct 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 76 additions & 7 deletions weblab/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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


Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 9 additions & 10 deletions weblab/core/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -48,7 +48,6 @@

running_experiment = Recipe('RunningExperiment', runnable=foreign_key(runnable))


dataset = Recipe('Dataset',
name=seq('my dataset'),
protocol=foreign_key(protocol))
Expand Down
15 changes: 15 additions & 0 deletions weblab/datasets/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions weblab/datasets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions weblab/entities/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
26 changes: 20 additions & 6 deletions weblab/entities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 8 additions & 8 deletions weblab/experiments/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,18 @@ 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.
Otherwise will create a new version of the experiment.
@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,
}
Expand All @@ -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)
Expand Down
Loading