diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5351a95e..9b03d490 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,15 @@
## Devel
+* Add filtering in list views.
+
+## 0.18 (2023-10-03)
+
* Include a workspace_data_object context variable for the `WorkspaceDetail` and `WorkspaceUpdate` views.
* Refactor auditing classes.
-* Add filtering in list views.
+* Add ability to specify a custom `Workspace` form in the workspace adapter.
+* Add informational text to tables on Detail pages.
+* Add a new "Limited view" permission. This permission is not yet used anywhere in the app, but can be used by projects using the app (e.g., to show a custom page to users with this permission).
## 0.17 (2023-07-11)
diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py
index f97bde71..c59266c2 100644
--- a/anvil_consortium_manager/__init__.py
+++ b/anvil_consortium_manager/__init__.py
@@ -1 +1 @@
-__version__ = "0.17.2dev1"
+__version__ = "0.19dev1"
diff --git a/anvil_consortium_manager/adapters/default.py b/anvil_consortium_manager/adapters/default.py
index 7e97531f..10cce327 100644
--- a/anvil_consortium_manager/adapters/default.py
+++ b/anvil_consortium_manager/adapters/default.py
@@ -17,6 +17,7 @@ class DefaultWorkspaceAdapter(BaseWorkspaceAdapter):
name = "Workspace"
type = "workspace"
description = "Default workspace"
+ workspace_form_class = forms.WorkspaceForm
workspace_data_model = models.DefaultWorkspaceData
workspace_data_form_class = forms.DefaultWorkspaceDataForm
list_table_class = tables.WorkspaceTable
diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py
index 39a17772..73a11746 100644
--- a/anvil_consortium_manager/adapters/workspace.py
+++ b/anvil_consortium_manager/adapters/workspace.py
@@ -4,6 +4,7 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+from django.forms import ModelForm
from django.utils.module_loading import import_string
from .. import models
@@ -36,6 +37,11 @@ def list_table_class(self):
"""Table class to use in a list of workspaces."""
...
+ @abstractproperty
+ def workspace_form_class(self):
+ """Custom form to use when creating a Workspace."""
+ ...
+
@abstractproperty
def workspace_data_model(self):
"""Model to use for storing extra data about workspaces."""
@@ -77,6 +83,22 @@ def get_list_table_class(self):
)
return self.list_table_class
+ def get_workspace_form_class(self):
+ """Return the form used to create a `Workspace`."""
+ if not self.workspace_form_class:
+ raise ImproperlyConfigured("Set `workspace_data_form_class`.")
+ # Make sure it is a model form
+ if not issubclass(self.workspace_form_class, ModelForm):
+ raise ImproperlyConfigured(
+ "workspace_form_class must be a subclass of ModelForm."
+ )
+ # Make sure it has the correct model set.
+ if self.workspace_form_class.Meta.model != models.Workspace:
+ raise ImproperlyConfigured(
+ "workspace_form_class Meta model field must be anvil_consortium_manager.models.Workspace."
+ )
+ return self.workspace_form_class
+
def get_workspace_data_model(self):
"""Return the `workspace_data_model`."""
if not self.workspace_data_model:
diff --git a/anvil_consortium_manager/anvil_audit.py b/anvil_consortium_manager/anvil_audit.py
deleted file mode 100644
index 1b1b25ad..00000000
--- a/anvil_consortium_manager/anvil_audit.py
+++ /dev/null
@@ -1,264 +0,0 @@
-"""Classes handling auditing of information in the Django database against AnVIL."""
-
-from abc import ABC, abstractproperty
-
-
-class AnVILAuditResults(ABC):
- """Abstract base class to store audit results from AnVIL."""
-
- @abstractproperty
- def allowed_errors(self):
- """List specifying the list of allowed errors for this audit result class."""
- ...
-
- def __init__(self):
- self.verified = set()
- self.errors = {}
- self.not_in_app = set()
-
- def add_not_in_app(self, record):
- """Add a record that is on ANVIL but is not in the app.
-
- Args:
- record (str): An identifier for the record that is not in the app.
- For example, for ManagedGroups, this will be the name of the group on AnVIL.
- """
- self.not_in_app.add(record)
-
- def add_error(self, model_instance, error):
- """Add an error for a Django model instance.
-
- Args:
- model_instance (obj): The Django model instance that had a detected error.
- error (str): The error that was detected.
-
- Raises:
- ValueError: If the `error` is not in the `allowed_errors` attribute of the class.
- """
- if error not in self.allowed_errors:
- raise ValueError("'{}' is not an allowed error.".format(error))
- if model_instance in self.verified:
- # Should this just remove it from verified and add the error instead?
- raise ValueError(
- "Cannot add error for model_instance {} that is already verified.".format(
- model_instance
- )
- )
- if model_instance in self.errors:
- self.errors[model_instance].append(error)
- else:
- self.errors[model_instance] = [error]
-
- def add_verified(self, model_instance):
- """Add a Django model instance that has been verified against AnVIL.
-
- Args:
- model_instance (obj): The Django model instance that was verified.
-
- Raises:
- ValueError: If the Django model instance being added has an error recorded in the `errors` attribute.
- """
- if model_instance in self.errors:
- raise ValueError("{} has reported errors.".format(model_instance))
- self.verified.add(model_instance)
-
- def get_verified(self):
- """Return a set of the verified records.
-
- Returns:
- set: The set of Django model instances that were verified against AnVIL.
- """
- return self.verified
-
- def get_errors(self):
- """Return the errors that were recorded in the audit.
-
- Returns:
- dict: A dictionary of errors.
-
- The keys of the dictionary are the Django model instances that had errors.
- The value for a given element is a list of the errors that were detected for that instance.
- """
- return self.errors
-
- def get_not_in_app(self):
- """Return records that are on AnVIL but not in the app.
-
- Returns:
- set: The records that exist on AnVIL but not in the app.
- """
- return self.not_in_app
-
- def ok(self):
- """Check if the audit results are ok.
-
- Returns:
- bool: An indicator of whether all audited records were successfully verified.
- """
- return not self.errors and not self.not_in_app
-
- def export(
- self, include_verified=True, include_errors=True, include_not_in_app=True
- ):
- """Return a dictionary representation of the audit results."""
- x = {}
- if include_verified:
- x["verified"] = [
- {"id": instance.pk, "instance": instance}
- for instance in self.get_verified()
- ]
- if include_errors:
- x["errors"] = [
- {"id": k.pk, "instance": k, "errors": v}
- for k, v in self.get_errors().items()
- ]
- if include_not_in_app:
- x["not_in_app"] = list(self.get_not_in_app())
- return x
-
-
-class BillingProjectAuditResults(AnVILAuditResults):
- """Class to hold audit results for :class:`~anvil_consortium_manager.models.BillingProject`.
-
- The elements of the set returned by ``get_verified()``
- and the keys of the dictionary returned by ``get_errors()``
- should all be :class:`~anvil_consortium_manager.models.BillingProject` model instances.
- """
-
- ERROR_NOT_IN_ANVIL = "Not in AnVIL"
- """Error when a BillingProject in the app does not exist in AnVIL."""
-
- # Set up allowed errors.
- allowed_errors = ERROR_NOT_IN_ANVIL
-
-
-class AccountAuditResults(AnVILAuditResults):
- """Class to hold audit results for :class:`~anviL_consortium_manager.models.Account`.
-
- The elements of the set returned by ``get_verified()``
- and the keys of the dictionary returned by ``get_errors()``
- should all be :class:`~anvil_consortium_manager.models.Account` model instances.
- """
-
- ERROR_NOT_IN_ANVIL = "Not in AnVIL"
- """Error when the Account does not exist in AnVIL."""
-
- # Set up allowed errors.
- allowed_errors = ERROR_NOT_IN_ANVIL
-
-
-class ManagedGroupAuditResults(AnVILAuditResults):
- """Class to hold audit results for :class:`~anviL_consortium_manager.models.ManagedGroup`s.
-
- The elements of the set returned by ``get_verified()``
- and the keys of the dictionary returned by ``get_errors()``
- should are be :class:`~anvil_consortium_manager.models.ManagedGroup` model instances.
- """
-
- ERROR_NOT_IN_ANVIL = "Not in AnVIL"
- """Error when a ManagedGroup in the app does not exist in AnVIL."""
-
- ERROR_DIFFERENT_ROLE = "App has a different role in this group"
- """Error when the service account running the app has a different role on AnVIL."""
-
- ERROR_GROUP_MEMBERSHIP = "Group membership does not match in AnVIL"
- """Error when a ManagedGroup has a different record of membership in the app compared to on AnVIL."""
-
- # Set up allowed errors.
- allowed_errors = (
- ERROR_NOT_IN_ANVIL,
- ERROR_DIFFERENT_ROLE,
- ERROR_GROUP_MEMBERSHIP,
- )
-
-
-class ManagedGroupMembershipAuditResults(AnVILAuditResults):
- """Class to hold audit results for the membership of a model instance of
- :class:`~anviL_consortium_manager.models.ManagedGroup`.
-
- The elements of the set returned by ``get_verified()``
- and the keys of the dictionary returned by ``get_errors()``
- should all be :class:`~anvil_consortium_manager.models.ManagedGroupMembership` model instances.
- """
-
- ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL = "Account not an admin in AnVIL"
- """Error when an Account is an admin of a ManagedGroup on the app, but not in AnVIL."""
-
- ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL = "Account not a member in AnVIL"
- """Error when an Account is a member of a ManagedGroup on the app, but not in AnVIL."""
-
- ERROR_GROUP_ADMIN_NOT_IN_ANVIL = "Group not an admin in AnVIL"
- """Error when a ManagedGroup is an admin of another ManagedGroup on the app, but not in AnVIL."""
-
- ERROR_GROUP_MEMBER_NOT_IN_ANVIL = "Group not a member in AnVIL"
- """Error when an ManagedGroup is a member of another ManagedGroup on the app, but not in AnVIL."""
-
- # Set up allowed errors.
- allowed_errors = (
- ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL,
- ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL,
- ERROR_GROUP_ADMIN_NOT_IN_ANVIL,
- ERROR_GROUP_MEMBER_NOT_IN_ANVIL,
- )
-
-
-class WorkspaceAuditResults(AnVILAuditResults):
- """Class to hold audit results for :class:`~anviL_consortium_manager.models.Workspace`.
-
- The elements of the set returned by ``get_verified()``
- and the keys of the dictionary returned by ``get_errors()``
- should all be :class:`~anvil_consortium_manager.models.Workspace` model instances.
- """
-
- ERROR_NOT_IN_ANVIL = "Not in AnVIL"
- """Error when a Workspace in the app does not exist on AnVIL."""
-
- ERROR_NOT_OWNER_ON_ANVIL = "Not an owner on AnVIL"
- """Error when the service account running the app is not an owner of the Workspace on AnVIL."""
-
- ERROR_DIFFERENT_AUTH_DOMAINS = "Has different auth domains on AnVIL"
- """Error when the Workspace has different auth domains in the app and on AnVIL."""
-
- ERROR_WORKSPACE_SHARING = "Workspace sharing does not match on AnVIL"
- """Error when a Workspace is shared with different ManagedGroups in the app and on AnVIL."""
-
- ERROR_DIFFERENT_LOCK = "Workspace lock status does not match on AnVIL"
- """Error when the workspace.is_locked status does not match the lock status on AnVIL."""
-
- # Set up allowed errors.
- allowed_errors = (
- ERROR_NOT_IN_ANVIL,
- ERROR_NOT_OWNER_ON_ANVIL,
- ERROR_DIFFERENT_AUTH_DOMAINS,
- ERROR_WORKSPACE_SHARING,
- ERROR_DIFFERENT_LOCK,
- )
-
-
-class WorkspaceGroupSharingAuditResults(AnVILAuditResults):
- """Class to hold audit results for group sharing to :class:`~anviL_consortium_manager.models.Workspace`s.
-
- The elements of the set returned by ``get_verified()``
- and the keys of the dictionary returned by ``get_errors()``
- should all be :class:`~anvil_consortium_manager.models.WorkspaceGroupSharing` model instances.
- """
-
- ERROR_NOT_SHARED_IN_ANVIL = "Not shared in AnVIL"
- """Error when a ManagedGroup has access to a workspace in the app but not on AnVIL."""
-
- ERROR_DIFFERENT_ACCESS = "Different access level in AnVIL"
- """Error when a ManagedGroup has a different access level for workspace in the app and on AnVIL."""
-
- ERROR_DIFFERENT_CAN_SHARE = "can_share value does not match in AnVIL"
- """Error when the can_share value for a ManagedGroup does not match what's on AnVIL."""
-
- ERROR_DIFFERENT_CAN_COMPUTE = "can_compute value does not match in AnVIL"
- """Error when the can_compute value for a ManagedGroup does not match what's on AnVIL."""
-
- # Set up allowed errors.
- allowed_errors = (
- ERROR_NOT_SHARED_IN_ANVIL,
- ERROR_DIFFERENT_ACCESS,
- ERROR_DIFFERENT_CAN_SHARE,
- ERROR_DIFFERENT_CAN_COMPUTE,
- )
diff --git a/anvil_consortium_manager/audit/__init__.py b/anvil_consortium_manager/audit/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/anvil_consortium_manager/auth.py b/anvil_consortium_manager/auth.py
index 2f1f94a0..0414710e 100644
--- a/anvil_consortium_manager/auth.py
+++ b/anvil_consortium_manager/auth.py
@@ -1,9 +1,24 @@
-from django.contrib.auth.mixins import PermissionRequiredMixin
+from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
from django.contrib.contenttypes.models import ContentType
from .models import AnVILProjectManagerAccess
+class AnVILConsortiumManagerLimitedViewRequired(UserPassesTestMixin):
+ """AnVIL global app limited view permission required mixin.
+
+ This mixin allows anyone with either LIMITED_VIEW or VIEW permission to access a view."""
+
+ def test_func(self):
+ apm_content_type = ContentType.objects.get_for_model(AnVILProjectManagerAccess)
+ perm_1 = f"{apm_content_type.app_label}.{AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME}"
+ perm_2 = f"{apm_content_type.app_label}.{AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME}"
+ has_perms = self.request.user.has_perms(
+ (perm_1,)
+ ) or self.request.user.has_perms((perm_2,))
+ return has_perms
+
+
class AnVILConsortiumManagerViewRequired(PermissionRequiredMixin):
"""AnVIL global app view permission required mixin"""
diff --git a/anvil_consortium_manager/forms.py b/anvil_consortium_manager/forms.py
index db880a84..14828136 100644
--- a/anvil_consortium_manager/forms.py
+++ b/anvil_consortium_manager/forms.py
@@ -153,20 +153,9 @@ class Meta:
fields = ("note",)
-class WorkspaceCreateForm(Bootstrap5MediaFormMixin, forms.ModelForm):
+class WorkspaceForm(Bootstrap5MediaFormMixin, forms.ModelForm):
"""Form to create a new workspace on AnVIL."""
- # Only allow billing groups where we can create a workspace.
- billing_project = forms.ModelChoiceField(
- queryset=models.BillingProject.objects.filter(has_app_as_user=True),
- widget=autocomplete.ModelSelect2(
- url="anvil_consortium_manager:billing_projects:autocomplete",
- attrs={"data-theme": "bootstrap-5"},
- ),
- help_text="""Select the billing project in which the workspace should be created.
- Only billing projects where this app is a user are shown.""",
- )
-
class Meta:
model = models.Workspace
fields = (
@@ -185,13 +174,14 @@ class Meta:
attrs={"data-theme": "bootstrap-5"},
),
}
- help_texts = {
- "billing_project": """Enter the billing project in which the workspace should be created.
- Only billing projects that have this app as a user are shown.""",
- "name": "Enter the name of the workspace to create.",
- "authorization_domains": """Select one or more authorization domains for this workspace.
- These cannot be changed after creation.""",
- }
+
+ def clean_billing_project(self):
+ billing_project = self.cleaned_data.get("billing_project")
+ if billing_project and not billing_project.has_app_as_user:
+ raise ValidationError(
+ "Billing project must have has_app_as_user set to True"
+ )
+ return billing_project
def clean(self):
# DJANGO <4.1 on mysql:
@@ -218,14 +208,6 @@ def clean(self):
return self.cleaned_data
-class WorkspaceUpdateForm(forms.ModelForm):
- """Form to update information about a Workspace."""
-
- class Meta:
- model = models.Workspace
- fields = ("note",)
-
-
class WorkspaceImportForm(forms.Form):
"""Form to import a workspace from AnVIL -- new version."""
diff --git a/anvil_consortium_manager/migrations/0013_alter_anvilprojectmanageraccess_options.py b/anvil_consortium_manager/migrations/0013_alter_anvilprojectmanageraccess_options.py
new file mode 100644
index 00000000..5e2b1c46
--- /dev/null
+++ b/anvil_consortium_manager/migrations/0013_alter_anvilprojectmanageraccess_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.12 on 2023-10-02 21:26
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('anvil_consortium_manager', '0012_managedgroup_email_unique'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='anvilprojectmanageraccess',
+ options={'default_permissions': (), 'managed': False, 'permissions': [('anvil_project_manager_edit', 'AnVIL Project Manager Edit Permission'), ('anvil_project_manager_view', 'AnVIL Project Manager View Permission'), ('anvil_project_manager_account_link', 'AnVIL Project Manager Account Link Permission'), ('anvil_project_manager_limited_view', 'AnVIL Project Manager Limited View Permission')]},
+ ),
+ ]
diff --git a/anvil_consortium_manager/models.py b/anvil_consortium_manager/models.py
index 2239c992..77b21070 100644
--- a/anvil_consortium_manager/models.py
+++ b/anvil_consortium_manager/models.py
@@ -22,6 +22,7 @@ class AnVILProjectManagerAccess(models.Model):
EDIT_PERMISSION_CODENAME = "anvil_project_manager_edit"
VIEW_PERMISSION_CODENAME = "anvil_project_manager_view"
+ LIMITED_VIEW_PERMISSION_CODENAME = "anvil_project_manager_limited_view"
ACCOUNT_LINK_PERMISSION_CODENAME = "anvil_project_manager_account_link"
class Meta:
@@ -39,6 +40,10 @@ class Meta:
"anvil_project_manager_account_link",
"AnVIL Project Manager Account Link Permission",
),
+ (
+ "anvil_project_manager_limited_view",
+ "AnVIL Project Manager Limited View Permission",
+ ),
]
@@ -674,7 +679,7 @@ class Workspace(TimeStampedModel):
max_length=64,
help_text="Name of the workspace on AnVIL, not including billing project name.",
)
- # This makes it possible to easily select the authorization domains in the WorkspaceCreateForm.
+ # This makes it possible to easily select the authorization domains in the WorkspaceForm.
# However, it does not create a record in django-simple-history for creating the many-to-many field.
authorization_domains = models.ManyToManyField(
"ManagedGroup",
diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/account_detail.html b/anvil_consortium_manager/templates/anvil_consortium_manager/account_detail.html
index 49d0350c..17ad8ea2 100644
--- a/anvil_consortium_manager/templates/anvil_consortium_manager/account_detail.html
+++ b/anvil_consortium_manager/templates/anvil_consortium_manager/account_detail.html
@@ -63,6 +63,10 @@
+
+ This table shows groups that the Account is directly in, either as a member or as an admin.
+
+
{% render_table group_table %}
@@ -82,6 +86,12 @@
+
+ This table shows Workspaces that the Account can access.
+ To be able to access a Workspace, a user must be a member of all authorization domains for that Workspace and the Workspace must be shared with a Managed Group that they are part of.
+ If a Workspace is shared with one Managed Group as a Reader and a second Managed Group as a writer, both records will appear in the table.
+
+
{% render_table accessible_workspace_table %}
diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/billingproject_detail.html b/anvil_consortium_manager/templates/anvil_consortium_manager/billingproject_detail.html
index ddb7f225..8d34097b 100644
--- a/anvil_consortium_manager/templates/anvil_consortium_manager/billingproject_detail.html
+++ b/anvil_consortium_manager/templates/anvil_consortium_manager/billingproject_detail.html
@@ -35,6 +35,9 @@
+
+ This table shows Workspaces that are in this Billing Project.
+
{% render_table workspace_table %}
diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html b/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html
index 63322087..eb83ea74 100644
--- a/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html
+++ b/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html
@@ -58,7 +58,10 @@
- {% render_table workspace_authorization_domain_table %}
+
+ This table shows Workspaces for which this group is used as an authorization domain.
+
+ {% render_table workspace_authorization_domain_table %}
@@ -79,6 +82,9 @@
+
+ This table shows Workspaces that have been shared directly with this group.
+
{% render_table workspace_table %}
@@ -99,6 +105,9 @@
+
+ This table shows Managed Groups that this group is a part of, either as an admin or a member.
+
{% render_table parent_table %}
@@ -115,6 +124,9 @@
+
+ This table shows Managed Groups that in this group, either as an admin or a member.
+
{% render_table group_table %}
@@ -130,6 +142,9 @@
+
+ This table shows active Accounts that in this group, either as an admin or a member.
+
{% render_table active_account_table %}
@@ -145,6 +160,10 @@
+
+ This table shows a record of the inactive Accounts that were in this group, either as an admin or a member.
+ Note that these Accounts are not part of the group on AnVIL.
+
{% render_table inactive_account_table %}
diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html b/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html
index 9c37c02d..16ec4f90 100644
--- a/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html
+++ b/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html
@@ -38,7 +38,10 @@
Authorization domains
- {% render_table authorization_domain_table %}
+
+ This table shows all Managed Groups that are used as an authorization domain for this Workspace.
+
+ {% render_table authorization_domain_table %}
@@ -54,6 +57,9 @@
+
+ This table shows Managed Groups that this Workspace has been shared with.
+
{% render_table group_sharing_table %}
diff --git a/anvil_consortium_manager/tests/test_adapters.py b/anvil_consortium_manager/tests/test_adapters.py
index 51ef47ad..27ea8fb7 100644
--- a/anvil_consortium_manager/tests/test_adapters.py
+++ b/anvil_consortium_manager/tests/test_adapters.py
@@ -1,5 +1,5 @@
from django.core.exceptions import ImproperlyConfigured
-from django.forms import ModelForm
+from django.forms import Form, ModelForm
from django.test import TestCase, override_settings
from ..adapters.account import BaseAccountAdapter
@@ -10,7 +10,7 @@
BaseWorkspaceAdapter,
WorkspaceAdapterRegistry,
)
-from ..forms import DefaultWorkspaceDataForm
+from ..forms import DefaultWorkspaceDataForm, WorkspaceForm
from ..models import Account, DefaultWorkspaceData
from ..tables import AccountTable, WorkspaceTable
from . import factories
@@ -105,6 +105,7 @@ class TestAdapter(BaseWorkspaceAdapter):
type = "test"
description = "test desc"
list_table_class = tables.TestWorkspaceDataTable
+ workspace_form_class = forms.WorkspaceForm
workspace_data_model = models.TestWorkspaceData
workspace_data_form_class = forms.TestWorkspaceDataForm
workspace_detail_template_name = "custom/workspace_detail.html"
@@ -132,6 +133,54 @@ def test_list_table_class_none(self):
with self.assertRaises(ImproperlyConfigured):
TestAdapter().get_list_table_class()
+ def test_get_workspace_form_class_default(self):
+ """get_workspace_form_class returns the correct form when using the default adapter."""
+ self.assertEqual(
+ DefaultWorkspaceAdapter().get_workspace_form_class(),
+ WorkspaceForm,
+ )
+
+ def test_get_workspace_form_class_custom(self):
+ """get_workspace_form_class returns the correct form when using a custom adapter."""
+ TestAdapter = self.get_test_adapter()
+ setattr(TestAdapter, "workspace_form_class", forms.TestWorkspaceForm)
+ self.assertEqual(
+ TestAdapter().get_workspace_form_class(), forms.TestWorkspaceForm
+ )
+
+ def test_get_workspace_form_class_none(self):
+ """get_workspace_form_class raises exception if form class is not set."""
+ TestAdapter = self.get_test_adapter()
+ setattr(TestAdapter, "workspace_form_class", None)
+ with self.assertRaises(ImproperlyConfigured):
+ TestAdapter().get_workspace_form_class()
+
+ def test_get_workspace_form_class_wrong_model(self):
+ """ImproperlyConfigured raised when wrong model is speciifed in Meta."""
+ TestAdapter = self.get_test_adapter()
+
+ class TestForm(ModelForm):
+ class Meta:
+ model = Account
+ fields = ("email",)
+
+ setattr(TestAdapter, "workspace_form_class", TestForm)
+ with self.assertRaises(ImproperlyConfigured) as e:
+ TestAdapter().get_workspace_form_class()
+ self.assertIn("workspace_form_class Meta model", str(e.exception))
+
+ def test_get_workspace_form_class_wrong_subclass(self):
+ """ImproperlyConfigured raised when form is not a ModelForm."""
+ TestAdapter = self.get_test_adapter()
+
+ class TestForm(Form):
+ pass
+
+ setattr(TestAdapter, "workspace_form_class", TestForm)
+ with self.assertRaises(ImproperlyConfigured) as e:
+ TestAdapter().get_workspace_form_class()
+ self.assertIn("ModelForm", str(e.exception))
+
def test_get_workspace_data_form_class_default(self):
"""get_workspace_data_form_class returns the correct form when using the default adapter."""
self.assertEqual(
@@ -299,6 +348,7 @@ class Adapter1(BaseWorkspaceAdapter):
type = "adapter1"
description = "one"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -308,6 +358,7 @@ class Adapter2(BaseWorkspaceAdapter):
type = "adapter2"
description = "two"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -329,6 +380,7 @@ class TestAdapter(BaseWorkspaceAdapter):
type = "adapter_type"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -349,6 +401,7 @@ class Adapter1(BaseWorkspaceAdapter):
type = "adapter_type"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -358,6 +411,7 @@ class Adapter2(BaseWorkspaceAdapter):
type = "adapter_type"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -395,6 +449,7 @@ class TestAdapter(BaseWorkspaceAdapter):
type = "adapter_type"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -412,6 +467,7 @@ class Adapter1(BaseWorkspaceAdapter):
type = "adapter_type"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -421,6 +477,7 @@ class Adapter2(BaseWorkspaceAdapter):
type = "adapter_type"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -456,6 +513,7 @@ class Adapter(BaseWorkspaceAdapter):
type = "adapter"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -473,6 +531,7 @@ class Adapter1(BaseWorkspaceAdapter):
type = "adapter1"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -482,6 +541,7 @@ class Adapter2(BaseWorkspaceAdapter):
type = "adapter2"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -507,6 +567,7 @@ class Adapter(BaseWorkspaceAdapter):
type = "adapter"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -524,6 +585,7 @@ class Adapter1(BaseWorkspaceAdapter):
type = "adapter1"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
@@ -533,6 +595,7 @@ class Adapter2(BaseWorkspaceAdapter):
type = "adapter2"
description = "desc"
list_table_class = None
+ workspace_form_class = None
workspace_data_model = None
workspace_data_form_class = None
workspace_detail_template_name = None
diff --git a/anvil_consortium_manager/tests/test_app/adapters.py b/anvil_consortium_manager/tests/test_app/adapters.py
index 5e8826ee..0a8428ce 100644
--- a/anvil_consortium_manager/tests/test_app/adapters.py
+++ b/anvil_consortium_manager/tests/test_app/adapters.py
@@ -11,6 +11,7 @@ class TestWorkspaceAdapter(BaseWorkspaceAdapter):
type = "test"
description = "Workspace type for testing"
list_table_class = tables.TestWorkspaceDataTable
+ workspace_form_class = forms.TestWorkspaceForm
workspace_data_model = models.TestWorkspaceData
workspace_data_form_class = forms.TestWorkspaceDataForm
workspace_detail_template_name = "test_workspace_detail.html"
diff --git a/anvil_consortium_manager/tests/test_app/forms.py b/anvil_consortium_manager/tests/test_app/forms.py
index 12a777e2..01451ee7 100644
--- a/anvil_consortium_manager/tests/test_app/forms.py
+++ b/anvil_consortium_manager/tests/test_app/forms.py
@@ -1,6 +1,9 @@
"""Forms classes for the example_site app."""
from django import forms
+from django.core.exceptions import ValidationError
+
+from anvil_consortium_manager.forms import WorkspaceForm
from . import models
@@ -14,3 +17,14 @@ class Meta:
"study_name",
"workspace",
)
+
+
+class TestWorkspaceForm(WorkspaceForm):
+ """Custom form for Workspace."""
+
+ def clean_name(self):
+ """Test custom cleaning for workspace name."""
+ name = self.cleaned_data.get("name")
+ if name and name == "test-fail":
+ raise ValidationError("Workspace name cannot be 'test-fail'!")
+ return name
diff --git a/anvil_consortium_manager/tests/test_anvil_audit.py b/anvil_consortium_manager/tests/test_audit.py
similarity index 100%
rename from anvil_consortium_manager/tests/test_anvil_audit.py
rename to anvil_consortium_manager/tests/test_audit.py
diff --git a/anvil_consortium_manager/tests/test_auth.py b/anvil_consortium_manager/tests/test_auth.py
new file mode 100644
index 00000000..e0ef3d22
--- /dev/null
+++ b/anvil_consortium_manager/tests/test_auth.py
@@ -0,0 +1,64 @@
+"""Tests for the auth.py source file classes that aren't tested elsewhere."""
+from django.contrib.auth.models import Permission, User
+from django.test import RequestFactory, TestCase
+
+from .. import auth, models
+
+
+class AnVILConsortiumManagerLimitedViewRequiredTest(TestCase):
+ """(Temporary) class to test the AnVILConsortiumManagerLimitedViewRequired mixin."""
+
+ def setUp(self):
+ """Set up test class."""
+ self.factory = RequestFactory()
+ self.user = User.objects.create_user(username="test", password="test")
+
+ def get_view_class(self):
+ return auth.AnVILConsortiumManagerLimitedViewRequired
+
+ def test_user_with_limited_view_perms(self):
+ """test_func returns True for a user with limited view permission."""
+ self.user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ inst = self.get_view_class()()
+ request = self.factory.get("")
+ request.user = self.user
+ inst.request = request
+ self.assertTrue(inst.test_func())
+
+ def test_user_with_view_perms(self):
+ """test_func returns True for a user with view permission."""
+ self.user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME
+ )
+ )
+ inst = self.get_view_class()()
+ request = self.factory.get("")
+ request.user = self.user
+ inst.request = request
+ self.assertTrue(inst.test_func())
+
+ def test_user_with_edit_perms(self):
+ """test_func returns False for a user with edit permission."""
+ self.user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME
+ )
+ )
+ inst = self.get_view_class()()
+ request = self.factory.get("")
+ request.user = self.user
+ inst.request = request
+ self.assertFalse(inst.test_func())
+
+ def test_user_with_no_perms(self):
+ """test_func returns False for a user with no permissions."""
+ inst = self.get_view_class()()
+ request = self.factory.get("")
+ request.user = self.user
+ inst.request = request
+ self.assertFalse(inst.test_func())
diff --git a/anvil_consortium_manager/tests/test_forms.py b/anvil_consortium_manager/tests/test_forms.py
index 230b55dd..a2def86e 100644
--- a/anvil_consortium_manager/tests/test_forms.py
+++ b/anvil_consortium_manager/tests/test_forms.py
@@ -302,10 +302,10 @@ def test_form_valid_note(self):
self.assertTrue(form.is_valid())
-class WorkspaceCreateFormTest(TestCase):
- """Tests for the WorkspaceCreateForm class."""
+class WorkspaceFormTest(TestCase):
+ """Tests for the WorkspaceForm class."""
- form_class = forms.WorkspaceCreateForm
+ form_class = forms.WorkspaceForm
def test_valid(self):
"""Form is valid with necessary input."""
@@ -375,8 +375,10 @@ def test_invalid_not_user_of_billing_project(self):
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn("billing_project", form.errors)
self.assertEqual(len(form.errors), 1)
+ self.assertIn("billing_project", form.errors)
+ self.assertEqual(len(form.errors["billing_project"]), 1)
+ self.assertIn("has_app_as_user", form.errors["billing_project"][0])
def test_invalid_case_insensitive_duplicate(self):
"""Cannot validate with the same case-insensitive name in the same billing project as an existing workspace."""
@@ -392,24 +394,6 @@ def test_invalid_case_insensitive_duplicate(self):
self.assertIn("already exists", form.errors[NON_FIELD_ERRORS][0])
-class WorkspaceUpdateFormTest(TestCase):
- """Tests for the ManagedGroupUpdateForm class."""
-
- form_class = forms.WorkspaceUpdateForm
-
- def test_valid(self):
- """Form is valid with necessary input."""
- form_data = {}
- form = self.form_class(data=form_data)
- self.assertTrue(form.is_valid())
-
- def test_form_valid_note(self):
- """Form is valid with the note field."""
- form_data = {"note": "test note"}
- form = self.form_class(data=form_data)
- self.assertTrue(form.is_valid())
-
-
class WorkspaceImportFormTest(TestCase):
form_class = forms.WorkspaceImportForm
diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py
index e5216352..65cf447f 100644
--- a/anvil_consortium_manager/tests/test_views.py
+++ b/anvil_consortium_manager/tests/test_views.py
@@ -75,6 +75,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -272,6 +287,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_context_data_anvil_status_ok(self):
"""Context data contains anvil_status."""
url_me = self.api_client.firecloud_entry_point + "/me?userDetailsOnly=true"
@@ -427,6 +457,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -671,6 +716,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, slug="foo")
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request, slug="foo")
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -771,6 +831,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_view_status_code_with_existing_object_not_user(self):
"""Returns a successful status code for an existing object pk."""
obj = factories.BillingProjectFactory.create(has_app_as_user=False)
@@ -868,6 +943,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_template_with_user_permission(self):
"""Returns successful response code."""
self.client.force_login(self.user)
@@ -1014,6 +1104,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_returns_all_objects(self):
"""Queryset returns all objects when there is no query."""
objects = factories.BillingProjectFactory.create_batch(10)
@@ -1143,6 +1248,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_template(self):
"""Template loads successfully."""
self.client.force_login(self.user)
@@ -1291,6 +1411,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_context_active_account(self):
"""An is_inactive flag is included in the context."""
active_account = factories.AccountFactory.create()
@@ -1634,6 +1769,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -1932,6 +2082,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, uuid=uuid)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
uuid = uuid4()
@@ -2039,6 +2204,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_user_with_perms_can_access_view(self):
"""Returns successful response code."""
self.client.force_login(self.user)
@@ -2600,12 +2780,26 @@ def test_access_without_user_permission(self):
user_no_perms = User.objects.create_user(
username="test-none", password="test-none"
)
- uuid = uuid4()
- request = self.factory.get(self.get_url(uuid, "bar"))
+ request = self.factory.get(self.get_url(uuid4(), "bar"))
request.user = user_no_perms
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4(), "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_user_with_perms_can_verify_email(self):
"""A user can successfully verify their email."""
email = "test@example.com"
@@ -2949,6 +3143,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_view_has_correct_table_class(self):
self.client.force_login(self.user)
response = self.client.get(self.get_url())
@@ -3122,6 +3331,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_view_has_correct_table_class(self):
self.client.force_login(self.user)
response = self.client.get(self.get_url())
@@ -3316,6 +3540,21 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_view_has_correct_table_class(self):
self.client.force_login(self.user)
response = self.client.get(self.get_url())
@@ -3544,6 +3783,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, uuid=uuid)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -3780,6 +4034,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, uuid=uuid)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -4085,6 +4354,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, pk=uuid)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -4334,6 +4618,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -4497,6 +4796,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -4634,6 +4948,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url(obj.name))
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -5060,6 +5389,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -5284,6 +5628,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, slug="foo")
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -5373,6 +5732,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -5525,6 +5899,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, pk=1)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -5919,6 +6308,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -6076,6 +6480,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -6331,6 +6750,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url(self.group.name))
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -6594,6 +7028,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ views.ManagedGroupVisualization.as_view()(request)
+
def test_view_status_code_with_existing_object_not_managed(self):
"""Returns a successful status code for an existing object pk."""
self.client.force_login(self.user)
@@ -6691,6 +7140,21 @@ def test_status_code_with_view_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ views.WorkspaceLandingPage.as_view()(request)
+
def test_status_code_with_edit_permission(self):
"""Returns successful response code."""
self.client.force_login(self.view_user)
@@ -6795,6 +7259,10 @@ def get_view(self):
"""Return the view being tested."""
return views.WorkspaceDetail.as_view()
+ def get_url(self, *args):
+ """Get the url for the view being tested."""
+ return reverse("anvil_consortium_manager:workspaces:detail", args=args)
+
def test_view_redirect_not_logged_in(self):
"View redirects to login view when user is not logged in."
# Need a client for redirects.
@@ -6811,15 +7279,27 @@ def test_status_code_with_user_permission(self):
response = self.client.get(obj.get_absolute_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
username="test-none", password="test-none"
)
- url = reverse(
- "anvil_consortium_manager:workspaces:detail", args=["foo1", "foo2"]
- )
- request = self.factory.get(url)
+ request = self.factory.get(self.get_url("foo", "bar"))
request.user = user_no_perms
with self.assertRaises(PermissionDenied):
self.get_view()(request)
@@ -7294,6 +7774,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, workspace_type=self.workspace_type)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(self.workspace_type))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -7323,7 +7818,11 @@ def test_has_form_in_context(self):
self.client.force_login(self.user)
response = self.client.get(self.get_url(self.workspace_type))
self.assertTrue("form" in response.context_data)
- self.assertIsInstance(response.context_data["form"], forms.WorkspaceCreateForm)
+
+ def test_form_class(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(self.workspace_type))
+ self.assertIsInstance(response.context_data["form"], forms.WorkspaceForm)
def test_has_formset_in_context(self):
"""Response includes a formset for the workspace_data model."""
@@ -7963,33 +8462,6 @@ def test_not_admin_of_auth_domain_on_anvil(self):
# Did not create any new Workspaces.
self.assertEqual(models.Workspace.objects.count(), 0)
- def test_not_user_of_billing_project(self):
- """Posting a billing project where we are not users does not create an object."""
- billing_project = factories.BillingProjectFactory.create(
- name="test-billing-project", has_app_as_user=False
- )
- self.client.force_login(self.user)
- response = self.client.post(
- self.get_url(self.workspace_type),
- {
- "billing_project": billing_project.pk,
- "name": "test-workspace",
- # Default workspace data for formset.
- "workspacedata-TOTAL_FORMS": 1,
- "workspacedata-INITIAL_FORMS": 0,
- "workspacedata-MIN_NUM_FORMS": 1,
- "workspacedata-MAX_NUM_FORMS": 1,
- },
- )
- self.assertEqual(response.status_code, 200)
- self.assertIn("form", response.context_data)
- form = response.context_data["form"]
- self.assertFalse(form.is_valid())
- self.assertIn("billing_project", form.errors.keys())
- self.assertIn("valid choice", form.errors["billing_project"][0])
- # No workspace was created.
- self.assertEqual(models.Workspace.objects.count(), 0)
-
def test_adapter_includes_workspace_data_formset(self):
"""Response includes the workspace data formset if specified."""
# Overriding settings doesn't work, because appconfig.ready has already run and
@@ -8096,6 +8568,50 @@ def test_adapter_does_not_create_objects_if_workspace_data_form_invalid(self):
self.assertEqual(app_models.TestWorkspaceData.objects.count(), 0)
self.assertEqual(len(responses.calls), 0)
+ def test_adapter_custom_form_class(self):
+ """No workspace is created if custom workspace form is invalid."""
+ # Overriding settings doesn't work, because appconfig.ready has already run and
+ # registered the default adapter. Instead, unregister the default and register the
+ # new adapter here.
+ workspace_adapter_registry.unregister(DefaultWorkspaceAdapter)
+ workspace_adapter_registry.register(TestWorkspaceAdapter)
+ self.workspace_type = "test"
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(self.workspace_type))
+ self.assertIsInstance(
+ response.context_data["form"], app_forms.TestWorkspaceForm
+ )
+
+ def test_adapter_does_not_create_object_if_workspace_form_invalid(self):
+ # Overriding settings doesn't work, because appconfig.ready has already run and
+ # registered the default adapter. Instead, unregister the default and register the
+ # new adapter here.
+ workspace_adapter_registry.unregister(DefaultWorkspaceAdapter)
+ workspace_adapter_registry.register(TestWorkspaceAdapter)
+ self.workspace_type = "test"
+ billing_project = factories.BillingProjectFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(self.workspace_type),
+ {
+ "billing_project": billing_project.pk,
+ "name": "test-fail",
+ # Default workspace data for formset.
+ "workspacedata-TOTAL_FORMS": 1,
+ "workspacedata-INITIAL_FORMS": 0,
+ "workspacedata-MIN_NUM_FORMS": 1,
+ "workspacedata-MAX_NUM_FORMS": 1,
+ "workspacedata-0-study_name": "test study",
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ form = response.context_data["form"]
+ self.assertFalse(form.is_valid())
+ self.assertIn("name", form.errors.keys())
+ self.assertIn("Workspace name cannot be", form.errors["name"][0])
+ self.assertEqual(models.Workspace.objects.count(), 0)
+ self.assertEqual(len(responses.calls), 0)
+
class WorkspaceImportTest(AnVILAPIMockTestMixin, TestCase):
"""Tests for the WorkspaceImport view."""
@@ -8239,6 +8755,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(self.workspace_type))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -9673,6 +10204,21 @@ def test_access_with_view_permission(self):
workspace_type=self.workspace_type,
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar", self.workspace_type))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -10722,7 +11268,22 @@ def test_access_with_view_permission(self):
request = self.factory.get(self.get_url("foo", "bar"))
request.user = user_with_view_perm
with self.assertRaises(PermissionDenied):
- self.get_view()(request, billing_project_slug="foo", workspace_slug="bar")
+ self.get_view()(request, billing_project_slug="foo", workspace_slug="bar")
+
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
@@ -10760,7 +11321,17 @@ def test_has_form_in_context(self):
self.get_url(self.workspace.billing_project.name, self.workspace.name)
)
self.assertIn("form", response.context_data)
- self.assertIsInstance(response.context_data["form"], forms.WorkspaceUpdateForm)
+ self.assertIsInstance(response.context_data["form"], forms.WorkspaceForm)
+
+ def test_form_fields(self):
+ """Response includes a form."""
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(self.workspace.billing_project.name, self.workspace.name)
+ )
+ form = response.context_data.get("form")
+ self.assertEqual(len(form.fields), 1)
+ self.assertIn("note", form.fields)
def test_has_formset_in_context(self):
"""Response includes a formset for the workspace_data model."""
@@ -10920,6 +11491,27 @@ def test_no_updates_if_invalid_workspace_data_form(self):
self.assertEqual(workspace.note, "original note")
self.assertEqual(workspace_data.study_name, "original name")
+ def test_custom_adapter_workspace_form(self):
+ """Workspace form is subclass of the custom adapter form."""
+ # Note that we need to use the test adapter for this.
+ workspace_adapter_registry.register(TestWorkspaceAdapter)
+ workspace = factories.WorkspaceFactory(
+ workspace_type=TestWorkspaceAdapter().get_type()
+ )
+ app_models.TestWorkspaceData.objects.create(
+ workspace=workspace, study_name="original name"
+ )
+ # Need a client for messages.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(workspace.billing_project.name, workspace.name)
+ )
+ self.assertTrue("form" in response.context_data)
+ form = response.context_data["form"]
+ self.assertIsInstance(form, TestWorkspaceAdapter().get_workspace_form_class())
+ self.assertEqual(len(form.fields), 1)
+ self.assertIn("note", form.fields)
+
class WorkspaceListTest(TestCase):
def setUp(self):
@@ -10965,6 +11557,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -11132,6 +11739,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url(self.workspace_type))
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(self.workspace_type))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -11359,6 +11981,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, pk=1)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -11712,6 +12349,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -11829,6 +12481,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url(self.default_workspace_type))
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(self.default_workspace_type))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -12068,6 +12735,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -12314,6 +12996,21 @@ def test_status_code_with_user_permission(self):
)
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -12548,6 +13245,21 @@ def test_status_code_with_user_permission(self):
)
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -12707,6 +13419,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -13345,6 +14072,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, parent_group_slug=self.parent_group.name)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -13876,6 +14618,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, group_slug=self.child_group.name)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -14379,6 +15136,21 @@ def test_access_with_view_permission(self):
child_group_slug=self.child_group.name,
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -14946,6 +15718,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -15068,6 +15855,21 @@ def test_access_with_view_permission(self):
request, parent_group_slug="parent", child_group_slug="child"
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -15385,6 +16187,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(obj.get_absolute_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -15543,6 +16360,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -16141,6 +16973,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, group_slug=self.group.name)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -16658,6 +17505,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, uuid=self.account.uuid)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url(uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -17162,6 +18024,21 @@ def test_access_with_view_permission(self):
request, group_slug=self.group.name, account_uuid=self.account.uuid
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -17672,6 +18549,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -17839,6 +18731,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -17973,6 +18880,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request, group_slug="foo", account_uuid=uuid)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", uuid4()))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -18265,6 +19187,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(obj.get_absolute_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar", "tmp"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -18432,6 +19369,21 @@ def test_access_with_view_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -19194,6 +20146,21 @@ def test_access_with_view_permission(self):
workspace_slug=self.workspace.name,
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -19944,6 +20911,21 @@ def test_access_with_view_permission(self):
group_slug=self.group.name,
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -20646,6 +21628,21 @@ def test_access_with_view_permission(self):
group_slug=self.group.name,
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar", "tmp"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -21450,6 +22447,21 @@ def test_access_with_view_permission(self):
group_slug="group",
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar", "tmp"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -21934,6 +22946,21 @@ def test_status_code_with_user_permission(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url())
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
@@ -22063,6 +23090,21 @@ def test_access_with_view_permission(self):
group_slug="group",
)
+ def test_access_with_limited_view_permission(self):
+ """Raises permission denied if user has limited view permission."""
+ user = User.objects.create_user(
+ username="test-limited", password="test-limited"
+ )
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME
+ )
+ )
+ request = self.factory.get(self.get_url("foo", "bar", "tmp"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
def test_access_without_user_permission(self):
"""Raises permission denied if user has no permissions."""
user_no_perms = User.objects.create_user(
diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py
index a65f4491..9a211828 100644
--- a/anvil_consortium_manager/views.py
+++ b/anvil_consortium_manager/views.py
@@ -1148,10 +1148,12 @@ class WorkspaceCreate(
viewmixins.WorkspaceAdapterMixin,
FormView,
):
- form_class = forms.WorkspaceCreateForm
success_message = "Successfully created Workspace on AnVIL."
template_name = "anvil_consortium_manager/workspace_create.html"
+ def get_form_class(self):
+ return self.adapter.get_workspace_form_class()
+
def get_workspace_data_formset(self):
"""Return an instance of the workspace data form to be used in this view."""
formset_prefix = "workspacedata"
@@ -1557,11 +1559,23 @@ class WorkspaceUpdate(
"""View to update information about an Account."""
model = models.Workspace
- form_class = forms.WorkspaceUpdateForm
slug_field = "name"
template_name = "anvil_consortium_manager/workspace_update.html"
success_message = "Successfully updated Workspace."
+ def get_form_class(self):
+ form_class = self.adapter.get_workspace_form_class()
+
+ class WorkspaceUpdateForm(form_class):
+ class Meta(form_class.Meta):
+ exclude = (
+ "billing_project",
+ "name",
+ "authorization_domains",
+ )
+
+ return WorkspaceUpdateForm
+
def get_object(self, queryset=None):
"""Return the object the view is displaying."""
diff --git a/docs/advanced.rst b/docs/advanced.rst
index d3ec12cd..eecbe82e 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -67,8 +67,10 @@ If you do not want to define a custom table, you can use the default table provi
Next, set up the adapter by subclassing :class:`~anvil_consortium_manager.adapter.BaseWorkspaceAdapter`. You will need to set:
-* ``type``: a string indicating the workspace type (e.g., ``"custom"``). This will be stored in the ``workspace_type`` field of the :class:`anvil_consortium_manager.models.Workspace` model for any workspaces created using the adapter.
* ``name``: a human-readable name for workspaces created with this adapater (e.g., ``"Custom workspace"``). This will be used when displaying information about workspaces created with this adapter.
+* ``type``: a string indicating the workspace type (e.g., ``"custom"``). This will be stored in the ``workspace_type`` field of the :class:`anvil_consortium_manager.models.Workspace` model for any workspaces created using the adapter.
+* ``description``: a string giving a brief description of the workspace data model. This will be displayed in the :class:`~anvil_consortium_manager.views.WorkspaceLandingPage` view.
+* ``workspace_form_class``: the form to use to create an instance of the ``Workspace`` model. The default adapter uses :class:`~anvil_consortium_manager.forms.WorkspaceForm``.
* ``workspace_data_model``: the model used to store additional data about a workspace, subclassed from :class:`~anvil_consortium_manager.models.BaseWorkspaceData`
* ``workspace_data_form_class``: the form to use to create an instance of the ``workspace_data_model``
* ``list_table_class``: the table to use to display the list of workspaces
@@ -83,16 +85,19 @@ Here is example of the custom adapter for ``my_app`` with the model, form and ta
.. code-block:: python
from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter
+ from anvil_consortium_manager.forms import WorkspaceForm
from my_app.models import CustomWorkspaceData
from my_app.forms import CustomWorkspaceDataForm
from my_app.tables import CustomWorkspaceTable
class CustomWorkspaceAdapter(BaseWorkspaceAdapter):
- type = "custom"
name = "Custom workspace"
+ type = "custom"
+ description = "Example custom workspace type for demo app"
+ list_table_class = tables.CustomWorkspaceDataTable
+ workspace_form_class = WorkspaceForm
workspace_data_model = models.CustomWorkspaceData
workspace_data_form_class = forms.CustomWorkspaceDataForm
- list_table_class = tables.CustomWorkspaceTable
workspace_detail_template_name = "my_app/custom_workspace_detail.html"
Finally, to tell the app to use this adapter, set ``ANVIL_WORKSPACE_ADAPTERS`` in your settings file, e.g.: ``ANVIL_WORKSPACE_ADAPTERS = ["my_app.adapters.CustomWorkspaceAdapter"]``. You can even define multiple adapters for different types of workspaces, e.g.:
@@ -120,3 +125,22 @@ If you would like to display information from the custom workspace data model in
{% endblock workspace_data %}
If custom content is not provided for the ``workspace_data`` block, a default set of information will be displayed: the billing project, the date added, and the date modified.
+
+Customizing the :class:`~anvil_consortium_manager.models.Workspace` form
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Most workspace adapters can set `workspace_data_form` to :class:`~anvil_consortium_manager.forms.WorkspaceForm`.
+This will use the default form provided by the app.
+
+If you would like to add a custom form (e.g., to provide custom help text or do additional cleaning on fields), you can set `workspace_data_form` to a custom form.
+You must subclass :class:`anvil_consortium_manager.forms.WorkspaceForm`.
+If you modify the form `Meta` class, make sure that it also subclasses `WorkspaceForm.Meta`:
+
+.. code-block:: python
+
+ from anvil_consortium_manager.forms import WorkspaceForm
+
+ class CustomWorkspaceForm(WorkspaceForm):
+
+ class Meta(WorkspaceForm.Meta):
+ help_texts = {"note": "Custom help for note field."}
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 61bbb9a5..abdfd3e7 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -15,7 +15,8 @@ Install from GitHub:
Configure
----------------------------------------------------------------------
-You will need a service account credentials file that is registered with Terra. XXX Get info from Ben about this.
+You will need a service account credentials file that is registered with Terra.
+See the `Terra service account documentation `_ for more information.
Required Settings
~~~~~~~~~~~~~~~~~
@@ -91,12 +92,14 @@ Post-installation
Permissions
~~~~~~~~~~~
-The app provides three different permissions settings.
+The app provides four different permissions settings.
-1. ``anvil_project_manager_view`` - users with this permission can view information, for example lists of users or workspace details.
+1. ``anvil_project_manager_edit`` - users with this permission can add, delete, or edit models, for example import an account from AnVIL or create a workspace.
-2. ``anvil_project_manager_edit`` - users with this permission can add, delete, or edit models, for example import an account from AnVIL or create a workspace.
+2. ``anvil_project_manager_view`` - users with this permission can view information, for example lists of users or workspace details.
3. ``anvil_project_manager_account_link`` - users with this permission can link their AnVIL accounts in the app using the `AccountLink` and `AccountLinkVerify` views.
+4. ``anvil_project_manager_limited_view`` - in the future, this permission will be used to allow users to view a limited set of information within the site. Rigt now, it is not used by the app.
+
We suggest creating three groups, viewers (with ``anvil_project_manager_view`` permission), editors (with both ``anvil_project_manager_view`` and ``anvil_project_manager_edit`` permission), and a final group for users who are allowed to link their AnVIL account. Users can then be added to the appropriate group. Note that users with edit permission but not view permission will not be able to see lists or detail pages, so anyone granted edit permission should also be granted view permission.
diff --git a/example_site/app/adapters.py b/example_site/app/adapters.py
index 686c4ac5..9d119e98 100644
--- a/example_site/app/adapters.py
+++ b/example_site/app/adapters.py
@@ -3,13 +3,14 @@
from . import forms, models, tables
-class ExampleWorkspaceAdapter(BaseWorkspaceAdapter):
+class CustomWorkspaceAdapter(BaseWorkspaceAdapter):
"""Example adapter for workspaces."""
- name = "Example workspace"
- type = "example"
- description = "Example workspace type for demo app"
- list_table_class = tables.ExampleWorkspaceDataTable
- workspace_data_model = models.ExampleWorkspaceData
- workspace_data_form_class = forms.ExampleWorkspaceDataForm
- workspace_detail_template_name = "app/example_workspace_detail.html"
+ name = "Custom workspace"
+ type = "custom"
+ description = "Example custom workspace type for demo app"
+ list_table_class = tables.CustomWorkspaceDataTable
+ workspace_form_class = forms.CustomWorkspaceForm
+ workspace_data_model = models.CustomWorkspaceData
+ workspace_data_form_class = forms.CustomWorkspaceDataForm
+ workspace_detail_template_name = "app/custom_workspace_detail.html"
diff --git a/example_site/app/admin.py b/example_site/app/admin.py
index db264968..f49ac240 100644
--- a/example_site/app/admin.py
+++ b/example_site/app/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from .models import ExampleWorkspaceData
+from .models import CustomWorkspaceData
# Register your models here.
-admin.site.register(ExampleWorkspaceData)
+admin.site.register(CustomWorkspaceData)
diff --git a/example_site/app/forms.py b/example_site/app/forms.py
index fa4f591c..fd366313 100644
--- a/example_site/app/forms.py
+++ b/example_site/app/forms.py
@@ -1,15 +1,33 @@
"""Forms classes for the example_site app."""
from django import forms
+from django.core.exceptions import ValidationError
+
+from anvil_consortium_manager.forms import WorkspaceForm
from . import models
-class ExampleWorkspaceDataForm(forms.ModelForm):
- """Form for an ExampleWorkspaceData object."""
+class CustomWorkspaceForm(WorkspaceForm):
+ """Example custom form for creating a Workspace."""
+
+ class Meta(WorkspaceForm.Meta):
+ help_texts = {
+ "name": "Enter the name of the workspace to create. (Hint: Example workspace names cannot include a 'y'.)",
+ }
+
+ def clean_name(self):
+ name = self.cleaned_data.get("name")
+ if name and "y" in name:
+ raise ValidationError("Name cannot include a y.")
+ return name
+
+
+class CustomWorkspaceDataForm(forms.ModelForm):
+ """Form for an CustomWorkspaceData object."""
class Meta:
- model = models.ExampleWorkspaceData
+ model = models.CustomWorkspaceData
fields = ("study_name", "consent_code", "workspace")
help_texts = {
"study_name": "Enter the name of the study associated with this workspace.",
diff --git a/example_site/app/migrations/0001_initial.py b/example_site/app/migrations/0001_initial.py
index 93720c03..c85a5a51 100644
--- a/example_site/app/migrations/0001_initial.py
+++ b/example_site/app/migrations/0001_initial.py
@@ -1,7 +1,9 @@
-# Generated by Django 3.2.12 on 2022-08-31 22:53
+# Generated by Django 3.2.12 on 2023-09-28 00:31
+from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
+import simple_history.models
class Migration(migrations.Migration):
@@ -9,12 +11,34 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('anvil_consortium_manager', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('anvil_consortium_manager', '0012_managedgroup_email_unique'),
]
operations = [
migrations.CreateModel(
- name='ExampleWorkspaceData',
+ name='HistoricalCustomWorkspaceData',
+ fields=[
+ ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
+ ('study_name', models.CharField(max_length=255)),
+ ('consent_code', models.CharField(max_length=16)),
+ ('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)),
+ ('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 custom workspace data',
+ 'verbose_name_plural': 'historical custom workspace datas',
+ 'ordering': ('-history_date', '-history_id'),
+ 'get_latest_by': ('history_date', 'history_id'),
+ },
+ bases=(simple_history.models.HistoricalChanges, models.Model),
+ ),
+ migrations.CreateModel(
+ name='CustomWorkspaceData',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('study_name', models.CharField(max_length=255)),
diff --git a/example_site/app/migrations/0002_historicalexampleworkspacedata.py b/example_site/app/migrations/0002_historicalexampleworkspacedata.py
deleted file mode 100644
index 69eadf09..00000000
--- a/example_site/app/migrations/0002_historicalexampleworkspacedata.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Generated by Django 3.2.12 on 2023-01-21 00:42
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import simple_history.models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('anvil_consortium_manager', '0006_historicaldefaultworkspacedata'),
- ('app', '0001_initial'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='HistoricalExampleWorkspaceData',
- fields=[
- ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
- ('study_name', models.CharField(max_length=255)),
- ('consent_code', models.CharField(max_length=16)),
- ('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)),
- ('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 example workspace data',
- 'verbose_name_plural': 'historical example workspace datas',
- 'ordering': ('-history_date', '-history_id'),
- 'get_latest_by': ('history_date', 'history_id'),
- },
- bases=(simple_history.models.HistoricalChanges, models.Model),
- ),
- ]
diff --git a/example_site/app/models.py b/example_site/app/models.py
index 3a070061..b675be81 100644
--- a/example_site/app/models.py
+++ b/example_site/app/models.py
@@ -4,7 +4,7 @@
# Create your models here.
-class ExampleWorkspaceData(BaseWorkspaceData):
+class CustomWorkspaceData(BaseWorkspaceData):
"""Example custom model to hold additional data about a Workspace."""
study_name = models.CharField(max_length=255)
diff --git a/example_site/app/tables.py b/example_site/app/tables.py
index 8d65bafc..348deacc 100644
--- a/example_site/app/tables.py
+++ b/example_site/app/tables.py
@@ -3,15 +3,15 @@
from anvil_consortium_manager import models as acm_models
-class ExampleWorkspaceDataTable(tables.Table):
+class CustomWorkspaceDataTable(tables.Table):
name = tables.columns.Column(linkify=True)
class Meta:
model = acm_models.Workspace
fields = (
- "exampleworkspacedata__study_name",
- "exampleworkspacedata__consent_code",
+ "customworkspacedata__study_name",
+ "customworkspacedata__consent_code",
"billing_project",
"name",
)
diff --git a/example_site/settings.py b/example_site/settings.py
index 731b96cb..92253f91 100644
--- a/example_site/settings.py
+++ b/example_site/settings.py
@@ -219,8 +219,8 @@
# Workspace adapters.
ANVIL_WORKSPACE_ADAPTERS = [
- "example_site.app.adapters.ExampleWorkspaceAdapter",
- # "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter",
+ "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter",
+ "example_site.app.adapters.CustomWorkspaceAdapter",
]
# Account adapter.
ANVIL_ACCOUNT_ADAPTER = (
diff --git a/example_site/templates/app/example_workspace_detail.html b/example_site/templates/app/custom_workspace_detail.html
similarity index 100%
rename from example_site/templates/app/example_workspace_detail.html
rename to example_site/templates/app/custom_workspace_detail.html