Skip to content

Commit

Permalink
Merge pull request #218 from UW-GAC/main
Browse files Browse the repository at this point in the history
Deploy to stage
  • Loading branch information
amstilp authored Mar 27, 2023
2 parents 1a65a49 + 9a54b0b commit 58eecf3
Show file tree
Hide file tree
Showing 14 changed files with 767 additions and 3 deletions.
2 changes: 1 addition & 1 deletion config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"gregor_django.utils.context_processors.settings_context",
"anvil_consortium_manager.context_processors.workspace_adapter",
],
},
}
Expand Down Expand Up @@ -357,6 +356,7 @@
"gregor_django.gregor_anvil.adapters.TemplateWorkspaceAdapter",
"gregor_django.gregor_anvil.adapters.UploadWorkspaceAdapter",
"gregor_django.gregor_anvil.adapters.CombinedConsortiumDataWorkspaceAdapter",
"gregor_django.gregor_anvil.adapters.ReleaseWorkspaceAdapter",
]
ANVIL_ACCOUNT_ADAPTER = "gregor_django.gregor_anvil.adapters.AccountAdapter"

Expand Down
18 changes: 18 additions & 0 deletions gregor_django/gregor_anvil/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class UploadWorkspaceAdapter(BaseWorkspaceAdapter):

type = "upload"
name = "Upload workspace"
description = "Workspaces that contain data uploaded by RCs in a given upload cycle"
list_table_class = tables.UploadWorkspaceTable
workspace_data_model = models.UploadWorkspace
workspace_data_form_class = forms.UploadWorkspaceForm
Expand All @@ -41,6 +42,9 @@ class ExampleWorkspaceAdapter(BaseWorkspaceAdapter):

type = "example"
name = "Example workspace"
description = (
"Workspaces that contain examples of using AnVIL, working with data, etc."
)
list_table_class = WorkspaceTable
workspace_data_model = models.ExampleWorkspace
workspace_data_form_class = forms.ExampleWorkspaceForm
Expand All @@ -52,6 +56,7 @@ class TemplateWorkspaceAdapter(BaseWorkspaceAdapter):

type = "template"
name = "Template workspace"
description = "Template workspaces that can be cloned to create other workspaces"
list_table_class = tables.TemplateWorkspaceTable
workspace_data_model = models.TemplateWorkspace
workspace_data_form_class = forms.TemplateWorkspaceForm
Expand All @@ -63,9 +68,22 @@ class CombinedConsortiumDataWorkspaceAdapter(BaseWorkspaceAdapter):

type = "combined_consortium"
name = "Combined consortium data workspace"
description = "Workspaces for internal consortium use that contain data tables combined across upload workspaces"
list_table_class = WorkspaceTable
workspace_data_model = models.CombinedConsortiumDataWorkspace
workspace_data_form_class = forms.CombinedConsortiumDataWorkspaceForm
workspace_detail_template_name = (
"gregor_anvil/combinedconsortiumdataworkspace_detail.html"
)


class ReleaseWorkspaceAdapter(BaseWorkspaceAdapter):
"""Adapter for ReleaseWorkspace."""

type = "release"
name = "Release workspace"
description = "Workspaces for release to the general scientific community via dbGaP"
list_table_class = tables.ReleaseWorkspaceTable
workspace_data_model = models.ReleaseWorkspace
workspace_data_form_class = forms.ReleaseWorkspaceForm
workspace_detail_template_name = "gregor_anvil/releaseworkspace_detail.html"
72 changes: 72 additions & 0 deletions gregor_django/gregor_anvil/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,75 @@ def clean_upload_workspaces(self):
ValidationError(self.ERROR_UPLOAD_VERSION_DOES_NOT_MATCH),
)
return data


class CustomDateInput(forms.widgets.DateInput):
input_type = "date"


class ReleaseWorkspaceForm(Bootstrap5MediaFormMixin, forms.ModelForm):
"""Form for a ReleaseWorkspace object."""

ERROR_CONSENT_DOES_NOT_MATCH = (
"Consent group must match consent group of upload workspaces."
)
ERROR_UPLOAD_WORKSPACE_CONSENT = (
"Consent group for upload workspaces must be the same."
)

class Meta:
model = models.ReleaseWorkspace
fields = (
"consent_group",
"upload_workspaces",
"full_data_use_limitations",
"dbgap_version",
"dbgap_participant_set",
"date_released",
"workspace",
)
help_texts = {
"upload_workspaces": """Upload workspaces contributing to this Release Workspace.
All upload workspaces must have the same consent group.""",
"date_released": """Do not select a date for this field unless the workspace has been
released to the scientific community.""",
}
widgets = {
# We considered checkboxes for workspaces with default all checked.
# Unfortunately we need to select only those with a given consent, not all workspaces.
# So go back to the ModelSelect2Multiple widget.
"upload_workspaces": autocomplete.ModelSelect2Multiple(
url="gregor_anvil:upload_workspaces:autocomplete",
attrs={"data-theme": "bootstrap-5"},
forward=["consent_group"],
),
# "date_released": forms.SelectDateInput(),
# "date_released": forms.DateInput(),
# "date_released": AdminDateWidget(),
"date_released": CustomDateInput(),
}

def clean_upload_workspaces(self):
"""Validate that all UploadWorkspaces have the same consent group."""
data = self.cleaned_data["upload_workspaces"]
versions = set([x.consent_group for x in data])
if len(versions) > 1:
self.add_error(
"upload_workspaces",
ValidationError(self.ERROR_UPLOAD_WORKSPACE_CONSENT),
)
return data

def clean(self):
"""Validate that consent_group matches the consent group for upload_workspaces."""
cleaned_data = super().clean()
# Make sure that the consent group specified matches the consent group for the upload_workspaces.
consent_group = cleaned_data.get("consent_group")
upload_workspaces = cleaned_data.get("upload_workspaces")
if consent_group and upload_workspaces:
# We only need to check the first workspace since the clean_upload_workspaces method checks
# that all upload_workspaces have the same consent.
if consent_group != upload_workspaces[0].consent_group:
raise ValidationError(self.ERROR_CONSENT_DOES_NOT_MATCH)

return cleaned_data
65 changes: 65 additions & 0 deletions gregor_django/gregor_anvil/migrations/0007_releaseworkspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 3.2.16 on 2023-03-27 18:47

from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
import simple_history.models


class Migration(migrations.Migration):

dependencies = [
('anvil_consortium_manager', '0008_workspace_is_locked'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gregor_anvil', '0006_combinedconsortiumdataworkspace_historicalcombinedconsortiumdataworkspace'),
]

operations = [
migrations.CreateModel(
name='ReleaseWorkspace',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('full_data_use_limitations', models.TextField(help_text='The full data use limitations for this workspace.')),
('dbgap_version', models.IntegerField(help_text='Version of the release (should be the same as dbGaP version).', validators=[django.core.validators.MinValueValidator(1)], verbose_name=' dbGaP version')),
('dbgap_participant_set', models.IntegerField(help_text='dbGaP participant set of the workspace', validators=[django.core.validators.MinValueValidator(1)], verbose_name=' dbGaP participant set')),
('date_released', models.DateField(blank=True, help_text='Date that this workspace was released to the scientific community.', null=True)),
('consent_group', models.ForeignKey(help_text='Consent group for the data in this workspace.', on_delete=django.db.models.deletion.PROTECT, to='gregor_anvil.consentgroup')),
('upload_workspaces', models.ManyToManyField(help_text='Upload workspaces contributing data to this workspace.', to='gregor_anvil.UploadWorkspace')),
('workspace', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='anvil_consortium_manager.workspace')),
],
),
migrations.CreateModel(
name='HistoricalReleaseWorkspace',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('full_data_use_limitations', models.TextField(help_text='The full data use limitations for this workspace.')),
('dbgap_version', models.IntegerField(help_text='Version of the release (should be the same as dbGaP version).', validators=[django.core.validators.MinValueValidator(1)], verbose_name=' dbGaP version')),
('dbgap_participant_set', models.IntegerField(help_text='dbGaP participant set of the workspace', validators=[django.core.validators.MinValueValidator(1)], verbose_name=' dbGaP participant set')),
('date_released', models.DateField(blank=True, help_text='Date that this workspace was released to the scientific community.', null=True)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('consent_group', models.ForeignKey(blank=True, db_constraint=False, help_text='Consent group for the data in this workspace.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='gregor_anvil.consentgroup')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='anvil_consortium_manager.workspace')),
],
options={
'verbose_name': 'historical release workspace',
'verbose_name_plural': 'historical release workspaces',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.AddConstraint(
model_name='releaseworkspace',
constraint=models.UniqueConstraint(fields=('consent_group', 'dbgap_version'), name='unique_release_workspace'),
),
]
49 changes: 49 additions & 0 deletions gregor_django/gregor_anvil/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,52 @@ class CombinedConsortiumDataWorkspace(TimeStampedModel, BaseWorkspaceData):
upload_workspaces = models.ManyToManyField(
UploadWorkspace, help_text="Upload workspaces"
)


class ReleaseWorkspace(TimeStampedModel, BaseWorkspaceData):
"""A model to track a workspace for release to the scientific community."""

phs = 3047
"""dbGaP-assigned phs for the GREGoR study."""

consent_group = models.ForeignKey(
ConsentGroup,
help_text="Consent group for the data in this workspace.",
on_delete=models.PROTECT,
)
upload_workspaces = models.ManyToManyField(
UploadWorkspace,
help_text="Upload workspaces contributing data to this workspace.",
)
full_data_use_limitations = models.TextField(
help_text="The full data use limitations for this workspace."
)
dbgap_version = models.IntegerField(
verbose_name=" dbGaP version",
validators=[MinValueValidator(1)],
help_text="Version of the release (should be the same as dbGaP version).",
)
dbgap_participant_set = models.IntegerField(
verbose_name=" dbGaP participant set",
validators=[MinValueValidator(1)],
help_text="dbGaP participant set of the workspace",
)
date_released = models.DateField(
null=True,
blank=True,
help_text="Date that this workspace was released to the scientific community.",
)

class Meta:
constraints = [
# Model uniqueness.
models.UniqueConstraint(
name="unique_release_workspace",
fields=["consent_group", "dbgap_version"],
),
]

def get_dbgap_accession(self):
return "phs{phs:06d}.v{v}.p{p}".format(
phs=self.phs, v=self.dbgap_version, p=self.dbgap_participant_set
)
25 changes: 25 additions & 0 deletions gregor_django/gregor_anvil/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,28 @@ class Meta:
def render_workspace_type(self, value):
adapter_names = workspace_adapter_registry.get_registered_names()
return adapter_names[value] + "s"


class ReleaseWorkspaceTable(tables.Table):
"""A table for Workspaces that includes fields from ReleaseWorkspace."""

name = tables.columns.Column(linkify=True)
number_workspaces = tables.columns.Column(
accessor="pk",
verbose_name="Number of workspaces",
orderable=False,
)

class Meta:
model = Workspace
fields = (
"name",
"releaseworkspace__consent_group",
"releaseworkspace__dbgap_version",
"releaseworkspace__dbgap_participant_set",
"number_workspaces",
"releaseworkspace__date_released",
)

def render_number_workspaces(self, record):
return record.releaseworkspace.upload_workspaces.count()
17 changes: 17 additions & 0 deletions gregor_django/gregor_anvil/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,20 @@ class Meta:
WorkspaceFactory,
workspace_type="combined_consortium",
)


class ReleaseWorkspaceFactory(DjangoModelFactory):
"""A factory for the ReleaseWorkspace model."""

class Meta:
model = models.ReleaseWorkspace

full_data_use_limitations = Faker("paragraph")
consent_group = SubFactory(ConsentGroupFactory)
dbgap_version = Faker("random_int", min=1, max=10)
dbgap_participant_set = Faker("random_int", min=1, max=10)

workspace = SubFactory(
WorkspaceFactory,
workspace_type="release",
)
Loading

0 comments on commit 58eecf3

Please sign in to comment.