diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c1ae0bf..4603487b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: pip install -r requirements/local.txt - name: Run tests - run: coverage run -p ./manage.py test --settings=config.settings.test + run: coverage run -p -m pytest env: # We can set this to an empty string, since we'll never make an API call. ANVIL_API_SERVICE_ACCOUNT_FILE: foo @@ -117,7 +117,7 @@ jobs: pip install -r requirements/local.txt - name: Run tests - run: coverage run -p ./manage.py test --settings=config.settings.test + run: coverage run -p -m pytest env: # We can set this to an empty string, since we'll never make an API call. ANVIL_API_SERVICE_ACCOUNT_FILE: foo diff --git a/gregor_django/drupal_oauth_provider/provider.py b/gregor_django/drupal_oauth_provider/provider.py index 1cb43c21..46a8f4d2 100644 --- a/gregor_django/drupal_oauth_provider/provider.py +++ b/gregor_django/drupal_oauth_provider/provider.py @@ -68,14 +68,9 @@ def get_provider_scope_config(self): provider_settings = app_settings.PROVIDERS.get(self.id, {}) gregor_oauth_scopes = provider_settings.get("SCOPES") - if not gregor_oauth_scopes: + if not gregor_oauth_scopes or not isinstance(gregor_oauth_scopes, list): raise ImproperlyConfigured( - f"[get_provider_scope_config] missing provider setting SCOPES {provider_settings}" - ) - - if not isinstance(gregor_oauth_scopes, list): - raise ImproperlyConfigured( - "[get_provider_scope_config] provider setting SCOPES should be a list" + f"[get_provider_scope_config] provider setting SCOPES {provider_settings} must be a list" ) return gregor_oauth_scopes diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py index 2145bca2..d822f1fc 100644 --- a/gregor_django/drupal_oauth_provider/tests.py +++ b/gregor_django/drupal_oauth_provider/tests.py @@ -5,6 +5,9 @@ from allauth.socialaccount.adapter import get_adapter from allauth.socialaccount.tests import OAuth2TestsMixin from allauth.tests import MockedResponse, TestCase +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.test import RequestFactory from django.test.utils import override_settings from .provider import CustomProvider @@ -125,3 +128,45 @@ def get_login_response_json(self, with_refresh_token=True): if with_refresh_token: response_data["refresh_token"] = "testrf" return json.dumps(response_data) + + +class TestProviderConfig(TestCase): + def test_custom_provider_scope_config(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = None + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + with self.assertRaises(ImproperlyConfigured): + CustomProvider(request).get_provider_scope_config() + + def test_custom_provider_scope_detail_config(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = [ + { + "z_drupal_machine_name": "X", + "request_scope": True, + "django_group_name": "Z", + } + ] + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + with self.assertRaises(ImproperlyConfigured): + CustomProvider(request).get_provider_managed_scope_status() + + def test_custom_provider_has_scope(self): + custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS + rf = RequestFactory() + request = rf.get("/fake-url/") + custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = [ + { + "drupal_machine_name": "X", + "request_scope": True, + "django_group_name": "Z", + } + ] + with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): + CustomProvider(request).get_provider_managed_scope_status( + scopes_granted=["X"] + ) diff --git a/gregor_django/drupal_oauth_provider/views.py b/gregor_django/drupal_oauth_provider/views.py index da428ba0..efb31d99 100644 --- a/gregor_django/drupal_oauth_provider/views.py +++ b/gregor_django/drupal_oauth_provider/views.py @@ -51,12 +51,6 @@ def _get_public_key_jwk(self, headers): def get_public_key(self, headers): - provider_settings = app_settings.PROVIDERS.get(self.provider_id, {}) - - config_public_key = provider_settings.get("PUBLIC_KEY") - if False and config_public_key: - return config_public_key - public_key_jwk = self._get_public_key_jwk(headers) try: public_key = jwt.algorithms.RSAAlgorithm.from_jwk( diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py index 313349b9..eeefdcdd 100644 --- a/gregor_django/gregor_anvil/tests/test_views.py +++ b/gregor_django/gregor_anvil/tests/test_views.py @@ -14,6 +14,8 @@ from django.test import RequestFactory, TestCase from django.urls import reverse +from gregor_django.users.tests.factories import UserFactory + from .. import models, tables, views from . import factories @@ -282,6 +284,22 @@ def test_view_status_code_with_invalid_pk(self): with self.assertRaises(Http404): self.get_view()(request, pk=obj.pk + 1) + def test_site_user_table(self): + """Contains a table of site users with the correct users.""" + obj = self.model_factory.create() + site_user = UserFactory.create() + site_user.research_centers.set([obj]) + non_site_user = UserFactory.create() + + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + self.assertIn("site_user_table", response.context_data) + table = response.context_data["site_user_table"] + self.assertEqual(len(table.rows), 1) + + self.assertIn(site_user, table.data) + self.assertNotIn(non_site_user, table.data) + class ResearchCenterListTest(TestCase): """Tests for the ResearchCenterList view.""" diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py index 22769192..dc65c780 100644 --- a/gregor_django/gregor_anvil/views.py +++ b/gregor_django/gregor_anvil/views.py @@ -1,12 +1,17 @@ from anvil_consortium_manager.auth import AnVILConsortiumManagerViewRequired from anvil_consortium_manager.models import Account, Workspace from dal import autocomplete +from django.contrib.auth import get_user_model from django.db.models import Count, Q from django.views.generic import DetailView, TemplateView -from django_tables2 import SingleTableView +from django_tables2 import SingleTableMixin, SingleTableView + +from gregor_django.users.tables import UserTable from . import models, tables +User = get_user_model() + class ConsentGroupDetail(AnVILConsortiumManagerViewRequired, DetailView): """View to show details about a `ConsentGroups`.""" @@ -21,10 +26,16 @@ class ConsentGroupList(AnVILConsortiumManagerViewRequired, SingleTableView): table_class = tables.ConsentGroupTable -class ResearchCenterDetail(AnVILConsortiumManagerViewRequired, DetailView): +class ResearchCenterDetail( + AnVILConsortiumManagerViewRequired, SingleTableMixin, DetailView +): """View to show details about a `ResearchCenter`.""" model = models.ResearchCenter + context_table_name = "site_user_table" + + def get_table(self): + return UserTable(User.objects.filter(research_centers=self.object)) class ResearchCenterList(AnVILConsortiumManagerViewRequired, SingleTableView): diff --git a/gregor_django/templates/gregor_anvil/researchcenter_detail.html b/gregor_django/templates/gregor_anvil/researchcenter_detail.html index a364849f..ac71eaeb 100644 --- a/gregor_django/templates/gregor_anvil/researchcenter_detail.html +++ b/gregor_django/templates/gregor_anvil/researchcenter_detail.html @@ -1,4 +1,5 @@ {% extends "anvil_consortium_manager/__object_detail.html" %} +{% load render_table from django_tables2 %} {% block title %}Research Center: {{ object.short_name }}{% endblock %} @@ -8,3 +9,9 @@
  • Short name: {{ object.short_name }}
  • {% endblock panel %} + +{% block after_panel %} +

    Research Center Users

    + {% render_table site_user_table %} +

    Research center user list only contains those site users who have created an account on this website.

    +{% endblock after_panel %} diff --git a/gregor_django/users/apps.py b/gregor_django/users/apps.py index 762949f6..4551c3e7 100644 --- a/gregor_django/users/apps.py +++ b/gregor_django/users/apps.py @@ -7,7 +7,4 @@ class UsersConfig(AppConfig): verbose_name = _("Users") def ready(self): - try: - import gregor_django.users.signals # noqa F401 - except ImportError: - pass + import gregor_django.users.signals # noqa F401 diff --git a/gregor_django/users/tables.py b/gregor_django/users/tables.py new file mode 100644 index 00000000..b6ab24e4 --- /dev/null +++ b/gregor_django/users/tables.py @@ -0,0 +1,15 @@ +import django_tables2 as tables +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class UserTable(tables.Table): + """A table for `User`s.""" + + username = tables.columns.Column(linkify=True) + research_centers = tables.columns.ManyToManyColumn(linkify_item=True) + + class Meta: + model = User + fields = ("username", "name", "email", "research_centers") diff --git a/gregor_django/users/tests/test_adapters.py b/gregor_django/users/tests/test_adapters.py index 4827e087..72a63fec 100644 --- a/gregor_django/users/tests/test_adapters.py +++ b/gregor_django/users/tests/test_adapters.py @@ -109,6 +109,14 @@ def test_update_research_centers_malformed(self): user, dict(research_center_or_site="FOO") ) + def test_update_user_research_centers_uknown(self): + adapter = SocialAccountAdapter() + user = UserFactory() + adapter.update_user_research_centers( + user, dict(research_center_or_site=["UNKNOWN"]) + ) + assert user.research_centers.all().count() == 0 + def test_update_user_groups_add(self): adapter = SocialAccountAdapter() rc1 = GroupFactory(name="g1") @@ -126,6 +134,22 @@ def test_update_user_groups_add(self): assert user.groups.filter(pk=rc1.pk).exists() assert user.groups.all().count() == 1 + def test_update_user_groups_create(self): + adapter = SocialAccountAdapter() + + User = get_user_model() + user = User() + setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, "test") + setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, "test@example.com") + + user.save() + + adapter.update_user_groups( + user, extra_data=dict(managed_scope_status={"CREATE_GROUP": True}) + ) + assert user.groups.filter(name="CREATE_GROUP").exists() + assert user.groups.all().count() == 1 + def test_update_user_groups_remove(self): adapter = SocialAccountAdapter() rc1 = GroupFactory(name="g1") diff --git a/gregor_django/users/tests/test_views.py b/gregor_django/users/tests/test_views.py index 6c6e19f8..3b966390 100644 --- a/gregor_django/users/tests/test_views.py +++ b/gregor_django/users/tests/test_views.py @@ -1,6 +1,7 @@ import pytest from django.conf import settings from django.contrib import messages +from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware @@ -9,11 +10,11 @@ from django.urls import reverse from gregor_django.users.forms import UserChangeForm -from gregor_django.users.models import User -from gregor_django.users.tests.factories import UserFactory from gregor_django.users.views import UserRedirectView, UserUpdateView, user_detail_view -pytestmark = pytest.mark.django_db +pytestmark = pytest.mark.django_db(transaction=True) + +User = get_user_model() class TestUserUpdateView: @@ -25,7 +26,7 @@ class TestUserUpdateView: https://github.com/pytest-dev/pytest-django/pull/258 """ - def dummy_get_response(self, request: HttpRequest): + def dummy_get_response(self, request: HttpRequest): # pragma: no cover return None def test_get_success_url(self, user: User, rf: RequestFactory): @@ -46,6 +47,14 @@ def test_get_object(self, user: User, rf: RequestFactory): assert view.get_object() == user + def test_user_update_view(self, client, user: User, rf: RequestFactory): + + client.force_login(user) + user_detail_url = reverse("users:update") + response = client.get(user_detail_url) + + assert response.status_code == 200 + def test_form_valid(self, user: User, rf: RequestFactory): view = UserUpdateView() request = rf.get("/fake-url/") @@ -78,11 +87,11 @@ def test_get_redirect_url(self, user: User, rf: RequestFactory): class TestUserDetailView: - def test_authenticated(self, user: User, rf: RequestFactory): - request = rf.get("/fake-url/") - request.user = UserFactory() + def test_authenticated(self, client, user: User, rf: RequestFactory): - response = user_detail_view(request, username=user.username) + client.force_login(user) + user_detail_url = reverse("users:detail", kwargs=dict(username=user.username)) + response = client.get(user_detail_url) assert response.status_code == 200 diff --git a/requirements/base.txt b/requirements/base.txt index 3d27f17a..5890ba2b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -21,7 +21,7 @@ django-dbbackup==4.0.1 # https://github.com/jazzband/django-dbbackup django-extensions==3.2.1 # https://github.com/django-extensions/django-extensions # anvil_consortium_manager -git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.12.1 +git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.13 # Simple history - model history tracking django-simple-history==3.1.1 # For tracking history diff --git a/requirements/local.txt b/requirements/local.txt index db7a4cc8..996aa95b 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -9,7 +9,7 @@ mypy==0.942 # https://github.com/python/mypy django-stubs==1.10.1 # https://github.com/typeddjango/django-stubs types-requests==2.27.25 pytest==7.2.0 # https://github.com/pytest-dev/pytest -pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar +pytest-sugar==0.9.6 # https://github.com/Frozenball/pytest-sugar responses==0.21.0 # https://github.com/getsentry/responses - for mocking HTTP responses # Documentation