Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds unit tests for research products #377

Merged
merged 5 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4672,7 +4672,7 @@ components:
- name
Grant:
type: object
description: Object serializer for the `Grant` class
description: Object serializer for the `Grant` class.
properties:
id:
type: integer
Expand Down Expand Up @@ -4829,7 +4829,7 @@ components:
type: boolean
PatchedGrant:
type: object
description: Object serializer for the `Grant` class
description: Object serializer for the `Grant` class.
properties:
id:
type: integer
Expand Down Expand Up @@ -4862,7 +4862,7 @@ components:
type: integer
PatchedPublication:
type: object
description: Object serializer for the `Publication` class
description: Object serializer for the `Publication` class.
properties:
id:
type: integer
Expand Down Expand Up @@ -4980,7 +4980,7 @@ components:
description: Specific permissions for this user.
Publication:
type: object
description: Object serializer for the `Publication` class
description: Object serializer for the `Publication` class.
properties:
id:
type: integer
Expand Down
8 changes: 4 additions & 4 deletions keystone_api/apps/research_products/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@

@admin.register(Publication)
class PublicationAdmin(admin.ModelAdmin):
"""Admin interface for the `Publication` class"""
"""Admin interface for the `Publication` class."""

@staticmethod
@admin.display
def title(obj: Publication) -> str:
"""Return a publication's title as a human/table friendly string"""
"""Return a publication's title as a human/table friendly string."""

# Rely on the object to determine the appropriate string title representation
return str(obj)
Expand All @@ -37,12 +37,12 @@ def title(obj: Publication) -> str:

@admin.register(Grant)
class GrantAdmin(admin.ModelAdmin):
"""Admin interface for the `Grant` class"""
"""Admin interface for the `Grant` class."""

@staticmethod
@admin.display
def amount(obj: Grant) -> str:
"""Return the allocation's service units as a human friendly string"""
"""Return the allocation's service units as a human friendly string."""

return f'${int(obj.amount):,}'

Expand Down
10 changes: 5 additions & 5 deletions keystone_api/apps/research_products/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@


class BaseManager(Manager):
"""Base manager class for abstracting common database filters"""
"""Base manager class for abstracting common database filters."""

def affiliated_with_user(self, user: User) -> models.QuerySet:
"""Get all allocation requests affiliated with the given user

Args:
user: The user to return affiliated records for
user: The user to return affiliated records for.

Return:
A filtered queryset
A filtered queryset.
"""

research_groups = ResearchGroup.objects.groups_for_user(user)
return self.get_queryset().filter(group__in=research_groups)


class GrantManager(BaseManager):
"""Object manager for the `Grant` database model"""
"""Object manager for the `Grant` database model."""


class PublicationManager(BaseManager):
"""Object manager for the `Publication` database model"""
"""Object manager for the `Publication` database model."""
8 changes: 4 additions & 4 deletions keystone_api/apps/research_products/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


class Grant(models.Model):
"""Metadata for a funding grant"""
"""Metadata for a funding grant."""

title = models.CharField(max_length=250)
agency = models.CharField(max_length=100)
Expand All @@ -31,13 +31,13 @@ class Grant(models.Model):
objects = GrantManager()

def __str__(self) -> str: # pragma: nocover
"""Return the grant title truncated to 50 characters"""
"""Return the grant title truncated to 50 characters."""

return truncatechars(self.title, 100)


class Publication(models.Model):
"""Metadata for an academic publication"""
"""Metadata for an academic publication."""

title = models.CharField(max_length=250)
abstract = models.TextField()
Expand All @@ -50,6 +50,6 @@ class Publication(models.Model):
objects = PublicationManager()

def __str__(self) -> str: # pragma: nocover
"""Return the publication title truncated to 50 characters"""
"""Return the publication title truncated to 50 characters."""

return truncatechars(self.title, 100)
17 changes: 8 additions & 9 deletions keystone_api/apps/research_products/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@


class CustomPermissionsBase(permissions.BasePermission):
"""Base manager class for abstracting request processing logic"""
"""Base manager class for common request processing logic."""

def get_research_group(self, request) -> ResearchGroup | None:
"""Return the research group indicated in the `group` filed of an incoming request
"""Return the research group indicated in the `group` filed of an incoming request.

Args:
request: The HTTP request
Expand All @@ -38,10 +38,10 @@ def get_research_group(self, request) -> ResearchGroup | None:


class GroupMemberAll(CustomPermissionsBase):
"""Permissions class for supplying read and write access to all users within the research group"""
"""Permissions class providing read and write access to all users within the research group."""

def has_permission(self, request, view) -> bool:
"""Return whether the request has permissions to access the requested resource"""
"""Return whether the request has permissions to access the requested resource."""

if request.method == 'TRACE' and not request.user.is_staff:
return False
Expand All @@ -50,17 +50,16 @@ def has_permission(self, request, view) -> bool:
return research_group is None or request.user in research_group.get_all_members()

def has_object_permission(self, request, view, obj):
"""Return whether the incoming HTTP request has permission to access a database record"""
"""Return whether the incoming HTTP request has permission to access a database record."""

return request.user in obj.group.get_all_members()


class GroupMemberReadGroupAdminWrite(CustomPermissionsBase):
"""Permissions class for supplying read access to regular users and read/write access to admin users within the
research group"""
"""Permissions class providing read access to regular users and read/write access to group admins."""

def has_permission(self, request, view) -> bool:
"""Return whether the request has permissions to access the requested resource"""
"""Return whether the request has permissions to access the requested resource."""

if request.method == 'TRACE' and not request.user.is_staff:
return False
Expand All @@ -69,7 +68,7 @@ def has_permission(self, request, view) -> bool:
return research_group is None or request.user in research_group.get_privileged_members()

def has_object_permission(self, request, view, obj):
"""Return whether the incoming HTTP request has permission to access a database record"""
"""Return whether the incoming HTTP request has permission to access a database record."""

read_only = request.method in permissions.SAFE_METHODS
is_group_member = request.user in obj.group.get_all_members()
Expand Down
8 changes: 4 additions & 4 deletions keystone_api/apps/research_products/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@


class PublicationSerializer(serializers.ModelSerializer):
"""Object serializer for the `Publication` class"""
"""Object serializer for the `Publication` class."""

class Meta:
"""Serializer settings"""
"""Serializer settings."""

model = Publication
fields = '__all__'
read_only = ['group']


class GrantSerializer(serializers.ModelSerializer):
"""Object serializer for the `Grant` class"""
"""Object serializer for the `Grant` class."""

class Meta:
"""Serializer settings"""
"""Serializer settings."""

model = Grant
fields = '__all__'
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Tests for the `GrantViewSet` class."""

from django.contrib.auth import get_user_model
from django.test import TestCase

from apps.research_products.models import Grant
from apps.research_products.views import GrantViewSet
from apps.users.models import ResearchGroup

User = get_user_model()


class GetQueryset(TestCase):
"""Test the filtering of grant records based on user status."""

def setUp(self) -> None:
"""Create user accounts and research grants."""

self.staff_user = User.objects.create_user(username='staff', password='foobar123!', is_staff=True)

Check warning on line 19 in keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py#L19

Possible hardcoded password: 'foobar123!'
self.general_user = User.objects.create_user(username='general', password='foobar123!')

Check warning on line 20 in keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py#L20

Possible hardcoded password: 'foobar123!'

self.group1_user = User.objects.create_user(username='user1', password='foobar123!')

Check warning on line 22 in keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py#L22

Possible hardcoded password: 'foobar123!'
self.group1 = ResearchGroup.objects.create(name='Group1', pi=self.group1_user)
self.group1_grant = Grant.objects.create(
title="Grant 1",
agency="Agency 1",
amount=100000.00,
grant_number="G-123",
fiscal_year=2020,
start_date="2020-01-01",
end_date="2021-01-01",
group=self.group1
)

self.group2_user = User.objects.create_user(username='user2', password='foobar123!')

Check warning on line 35 in keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_GrantViewSet.py#L35

Possible hardcoded password: 'foobar123!'
self.group2 = ResearchGroup.objects.create(name='Group2', pi=self.group2_user)
self.group2_grant = Grant.objects.create(
title="Grant 2",
agency="Agency 2",
amount=200000.00,
grant_number="G-456",
fiscal_year=2021,
start_date="2021-01-01",
end_date="2022-01-01",
group=self.group2
)

def create_viewset(self, user: User) -> GrantViewSet:
"""
Create a viewset for testing purposes.

Args:
user: The user submitting a request to the viewset.

Returns:
A viewset instance tied to a request from the given user.
"""

viewset = GrantViewSet()
viewset.request = self.client.request().wsgi_request
viewset.request.user = user
return viewset

def test_queryset_for_staff_user(self) -> None:
"""Test staff users can view all grants."""

viewset = self.create_viewset(self.staff_user)
queryset = viewset.get_queryset()
self.assertEqual(len(queryset), 2)

def test_queryset_for_group_member(self) -> None:
"""Test group members can only access their group's grants."""

viewset = self.create_viewset(self.group1_user)
queryset = viewset.get_queryset()
self.assertEqual(len(queryset), 1)
self.assertEqual(queryset[0].group, self.group1)

def test_queryset_for_non_group_member(self) -> None:
"""Test users without groups cannot access any grant records."""

viewset = self.create_viewset(self.general_user)
queryset = viewset.get_queryset()
self.assertEqual(len(queryset), 0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Tests for the `PublicationViewSet` class."""

from django.contrib.auth import get_user_model
from django.test import TestCase

from apps.research_products.models import Publication
from apps.research_products.views import PublicationViewSet
from apps.users.models import ResearchGroup

User = get_user_model()


class GetQueryset(TestCase):
"""Test the filtering of records based on user status."""

def setUp(self) -> None:
"""Create user accounts and research publications."""

self.staff_user = User.objects.create_user(username='staff', password='foobar123!', is_staff=True)

Check warning on line 19 in keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py#L19

Possible hardcoded password: 'foobar123!'
self.general_user = User.objects.create_user(username='general', password='foobar123!')

Check warning on line 20 in keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py#L20

Possible hardcoded password: 'foobar123!'

self.group1_user = User.objects.create_user(username='user1', password='foobar123!')

Check warning on line 22 in keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py#L22

Possible hardcoded password: 'foobar123!'
self.group1 = ResearchGroup.objects.create(name='Group1', pi=self.group1_user)
self.group1_publication = Publication.objects.create(
title="Publication 1",
abstract="Abstract 1",
date="2020-01-01",
journal="Journal 1",
group=self.group1
)

self.group2_user = User.objects.create_user(username='user2', password='foobar123!')

Check warning on line 32 in keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

keystone_api/apps/research_products/tests/test_views/test_PublicationViewSet.py#L32

Possible hardcoded password: 'foobar123!'
self.group2 = ResearchGroup.objects.create(name='Group2', pi=self.group2_user)
self.group2_publication = Publication.objects.create(
title="Publication 2",
abstract="Abstract 2",
date="2020-01-02",
journal="Journal 2",
group=self.group2
)

def create_viewset(self, user: User) -> PublicationViewSet:
"""Create a viewset for testing purposes.

Args:
user: The user submitting a request to the viewset.

Returns:
A viewset instance tied to a request from the given user.
"""

viewset = PublicationViewSet()
viewset.request = self.client.request().wsgi_request
viewset.request.user = user
return viewset

def test_queryset_for_staff_user(self) -> None:
"""Test staff users can view all publications."""

viewset = self.create_viewset(self.staff_user)
queryset = viewset.get_queryset()
self.assertEqual(len(queryset), 2)

def test_queryset_for_group_member(self) -> None:
"""Test user's can only access their group's publications."""

viewset = self.create_viewset(self.group1_user)
queryset = viewset.get_queryset()
self.assertEqual(len(queryset), 1)
self.assertEqual(queryset[0].group, self.group1)

def test_queryset_for_non_group_member(self) -> None:
"""Test user's without groups cannot access any records."""

viewset = self.create_viewset(self.general_user)
queryset = viewset.get_queryset()
self.assertEqual(len(queryset), 0)
2 changes: 1 addition & 1 deletion keystone_api/apps/research_products/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""URL routing for the parent application"""
"""URL routing for the parent application."""

from rest_framework.routers import DefaultRouter

Expand Down
4 changes: 2 additions & 2 deletions keystone_api/apps/research_products/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class PublicationViewSet(viewsets.ModelViewSet):
]

def get_queryset(self) -> list[Publication]:
"""Return a list of allocation requests for the currently authenticated user"""
"""Return a list of allocation requests for the currently authenticated user."""

if self.request.user.is_staff:
return self.queryset
Expand All @@ -43,7 +43,7 @@ class GrantViewSet(viewsets.ModelViewSet):
]

def get_queryset(self) -> list[Grant]:
"""Return a list of allocation requests for the currently authenticated user"""
"""Return a list of allocation requests for the currently authenticated user."""

if self.request.user.is_staff:
return self.queryset
Expand Down
Loading