Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
Expand Down
56 changes: 56 additions & 0 deletions weblab/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ def add_version(entity,
populate_entity_cache(entity)
return commit

@staticmethod
def cached_version(entity, **kwargs):
"""Add a single commit/version to an entity and 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)

@staticmethod
def add_fake_version(entity, visibility='private', date=None, message='cache-only commit'):
"""Add a new commit/version only in the cache, not in git."""
Expand Down Expand Up @@ -140,6 +147,13 @@ def protocol_with_version():
return protocol


@pytest.fixture
def fittingspec_with_version():
fittingspec = recipes.fittingspec.make()
Helpers.add_version(fittingspec, visibility='private')
return fittingspec


@pytest.fixture
def public_model(helpers):
model = recipes.model.make()
Expand All @@ -154,6 +168,19 @@ def public_protocol(helpers):
return protocol


@pytest.fixture
def public_fittingspec(helpers):
fittingspec = recipes.fittingspec.make()
helpers.add_version(fittingspec, visibility='public')
return fittingspec


@pytest.fixture
def public_dataset(helpers):
dataset = recipes.dataset.make(visibility='public')
return dataset


@pytest.fixture
def moderated_model(helpers):
model = recipes.model.make()
Expand Down Expand Up @@ -331,3 +358,32 @@ def my_dataset_with_file(logged_in_user, helpers, public_protocol, client):
)
yield dataset
dataset.delete()


@pytest.fixture
def fittingresult_version(public_model, public_protocol, public_fittingspec, public_dataset):
return recipes.fittingresult_version.make(
status='SUCCESS',
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,
)


@pytest.fixture
def fittingresult_with_result(model_with_version, protocol_with_version):
version = recipes.fittingresult_version.make(
status='SUCCESS',
fittingresult__model=model_with_version,
fittingresult__model_version=model_with_version.repocache.latest_version,
fittingresult__protocol=protocol_with_version,
fittingresult__protocol_version=protocol_with_version.repocache.latest_version,
)
version.mkdir()
with (version.abs_path / 'result.txt').open('w') as f:
f.write('fitting results')
return version
17 changes: 17 additions & 0 deletions weblab/core/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
cached_protocol_version = Recipe('CachedProtocolVersion')
cached_protocol_tag = Recipe('CachedProtocolTag')

cached_fittingspec = Recipe('CachedFittingSpec')
cached_fittingspec_version = Recipe('CachedFittingSpecVersion')
cached_fittingspec_tag = Recipe('CachedFittingSpecTag')

experiment = Recipe(
'Experiment',
model=foreign_key(model),
Expand All @@ -51,3 +55,16 @@

dataset_file = Recipe('DatasetFile',
dataset=foreign_key(dataset))

fittingresult = Recipe(
'FittingResult',
model=foreign_key(model),
model_version=foreign_key(cached_model_version),
protocol=foreign_key(protocol),
protocol_version=foreign_key(cached_protocol_version),
fittingspec=foreign_key(fittingspec),
fittingspec_version=foreign_key(cached_fittingspec_version),
dataset=foreign_key(dataset),
)

fittingresult_version = Recipe('FittingResultVersion', fittingresult=foreign_key(fittingresult))
141 changes: 81 additions & 60 deletions weblab/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,75 @@
from repocache.models import CachedModelVersion, CachedProtocolVersion


class Experiment(UserCreatedModelMixin, models.Model):
class ExperimentMixin(models.Model):
"""
Model mixin for different types of experiment

Models must have model, model_version, protocol and protocol_version fields
and be the parent of a Runnable-derived model.
"""
def __str__(self):
return self.name

@property
def latest_version(self):
return self.versions.latest('created_at')

@property
def nice_model_version(self):
"""Use tags to give a nicer representation of the commit id"""
return self.model_version.nice_version()

@property
def nice_protocol_version(self):
"""Use tags to give a nicer representation of the commit id"""
return self.protocol_version.nice_version()

@property
def latest_result(self):
try:
return self.latest_version.status
except Runnable.DoesNotExist:
return ''

@property
def entities(self):
"""Entity objects related to this experiment"""
return (self.model, self.protocol)

def is_visible_to_user(self, user):
"""
Can the user view the experiment?

:param user: user to test against

:returns: True if the user is allowed to view the experiment, False otherwise
"""
return visibility_check(self.visibility, self.viewers, user)

@property
def viewers(self):
"""
Get users which have special permissions to view this experiment.

We take the intersection of users with special permissions to view each object
(model, fitting spec, etc) involved, if that object is private. If it's public,
we can ignore it because everyone can see it.

:return: `set` of `User` objects
"""
viewers = [
obj.viewers
for obj in self.entities
if obj.visibility == Visibility.PRIVATE
]
return set.intersection(*viewers) if viewers else {}

class Meta:
abstract = True


class Experiment(ExperimentMixin, UserCreatedModelMixin, models.Model):
"""A specific version of a protocol run on a specific version of a model

This class essentially just stores the model & protocol links. The results are
Expand All @@ -34,9 +102,6 @@ class Meta:
('create_experiment', 'Can create experiments'),
)

def __str__(self):
return self.name

@property
def name(self):
return self.get_name()
Expand All @@ -60,55 +125,6 @@ def get_name(self, model_version=False, proto_version=False):
def visibility(self):
return get_joint_visibility(self.model_version.visibility, self.protocol_version.visibility)

@property
def viewers(self):
"""
Get users which have special permissions to view this experiment

We do not handle the case where both model and protocol are public,
since this would make the experiment also public and therefore
visible to every user - so calling this method makes very little sense.

:return: `set` of `User` objects
"""
if self.protocol.visibility != Visibility.PRIVATE:
return self.model.viewers
elif self.model.visibility != Visibility.PRIVATE:
return self.protocol.viewers
else:
return self.model.viewers & self.protocol.viewers

def is_visible_to_user(self, user):
"""
Can the user view the experiment?

:param user: user to test against

:returns: True if the user is allowed to view the experiment, False otherwise
"""
return visibility_check(self.visibility, self.viewers, user)

@property
def latest_version(self):
return self.versions.latest('created_at')

@property
def nice_model_version(self):
"""Use tags to give a nicer representation of the commit id"""
return self.model_version.nice_version()

@property
def nice_protocol_version(self):
"""Use tags to give a nicer representation of the commit id"""
return self.protocol_version.nice_version()

@property
def latest_result(self):
try:
return self.latest_version.status
except Runnable.DoesNotExist:
return ''


class Runnable(UserCreatedModelMixin, FileCollectionMixin, models.Model):
""" Runnable base class
Expand Down Expand Up @@ -140,33 +156,38 @@ class Runnable(UserCreatedModelMixin, FileCollectionMixin, models.Model):
)
return_text = models.TextField(blank=True)

def __str__(self):
return '%s at %s: (%s)' % (self.experiment, self.created_at, self.status)

class Meta:
indexes = [
models.Index(fields=['created_at'])
]

def __str__(self):
return '%s at %s: (%s)' % (self.parent, self.created_at, self.status)

@property
def parent(self):
"""E.g. the Experiment this is a version of. Must be defined by subclasses."""
raise NotImplementedError

@property
def name(self):
return '{:%Y-%m-%d %H:%M:%S}'.format(self.created_at)

@property
def run_number(self):
return self.experiment.versions.filter(created_at__lte=self.created_at).count()
return self.parent.versions.filter(created_at__lte=self.created_at).count()

@property
def is_latest(self):
return not self.experiment.versions.filter(created_at__gt=self.created_at).exists()
return not self.parent.versions.filter(created_at__gt=self.created_at).exists()

@property
def visibility(self):
return self.experiment.visibility
return self.parent.visibility

@property
def viewers(self):
return self.experiment.viewers
return self.parent.viewers

@property
def abs_path(self):
Expand Down
Loading