Skip to content
Snippets Groups Projects
Commit 55948aac authored by florimondmanca's avatar florimondmanca
Browse files

add Student to the API, refactor and improve the field_test util, add ModelTestCase, various fixes

parent 8b2d6707
Branches
No related tags found
No related merge requests found
......@@ -2,8 +2,9 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from persons.models import Tutor
# from api.serializers.users import UserSerializer
from persons.models import Tutor, Student
from tutoring.models import TutoringGroup
class TutorSerializer(serializers.HyperlinkedModelSerializer):
......@@ -28,3 +29,26 @@ class TutorSerializer(serializers.HyperlinkedModelSerializer):
'view_name': 'api:tutor-detail',
}
}
class StudentSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for Student."""
user = serializers.HyperlinkedRelatedField(
queryset=get_user_model().objects.all(),
view_name='api:user-detail',
)
tutoring_group = serializers.HyperlinkedRelatedField(
queryset=TutoringGroup.objects.all(),
view_name='api:tutoringgroup-detail',
)
class Meta: # noqa
model = Student
fields = ('user', 'address', 'tutoring_group',)
extra_kwargs = {
'url': {
'view_name': 'api:student-detail',
}
}
......@@ -12,10 +12,15 @@ class TutoringGroupSerializer(serializers.HyperlinkedModelSerializer):
read_only=True,
view_name='api:tutor-detail',
)
students = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name='api:student-detail',
)
class Meta: # noqa
model = TutoringGroup
fields = ('name', 'tutors',)
fields = ('name', 'tutors', 'students')
extra_kwargs = {
'url': {
'view_name': 'api:tutoringgroup-detail',
......
......@@ -16,6 +16,7 @@ urlpatterns = [
router = routers.DefaultRouter()
router.register(r'users', users.UserViewSet)
router.register(r'tutors', persons.TutorViewSet)
router.register(r'students', persons.StudentViewSet)
router.register(r'tutoringgroups', tutoring.TutoringGroupViewSet)
urlpatterns += router.urls
......@@ -5,8 +5,8 @@ from rest_framework.mixins import (
ListModelMixin, RetrieveModelMixin, CreateModelMixin,
)
from persons.models import Tutor
from api.serializers.persons import TutorSerializer
from persons.models import Tutor, Student
from api.serializers.persons import TutorSerializer, StudentSerializer
# Create your views here.
......@@ -29,3 +29,23 @@ class TutorViewSet(ListModelMixin,
queryset = Tutor.objects.all()
serializer_class = TutorSerializer
class StudentViewSet(ListModelMixin,
RetrieveModelMixin,
CreateModelMixin,
viewsets.GenericViewSet):
"""API endpoint that allows students to be viewed or edited.
retrieve:
Return a student instance.
list:
Return all students.
create:
Create a student instance.
"""
queryset = Student.objects.all()
serializer_class = StudentSerializer
......@@ -35,7 +35,8 @@ class Student(models.Model):
tutoring_group = models.ForeignKey('tutoring.TutoringGroup',
on_delete=models.SET_NULL,
null=True)
null=True,
related_name='students')
def get_absolute_url(self):
return reverse('api:student-detail', args=[str(self.id)])
......
from .misc import (
random_email, random_username,
)
from .field_test import FieldTestCase
from .test_cases import FieldTestCase, ModelTestCase
"""Definition of FieldTestCase.
Usage
-----
class PhoneFieldTestCase(FieldTestCase):
'''Test phone_number field of User.'''
model = User
field_name = 'phone_number'
tests = {
'verbose_name': 'numéro de téléphone',
'blank': True,
'unique': False,
}
Supported attribute tests
-------------------------
Boolean value test:
'unique': True
String equality test:
'verbose_name': 'my-field-verbose-name'
"""Definition of field tests.
See FieldTestMeta docstring for available field tests.
"""
from django.test import TestCase
from numbers import Number
# Toggle to True to print a message when a field test method is called.
PRINT_FIELD_TEST_CALLS = False
class UnsupportedFieldTest(ValueError):
"""Raised when trying to use an unsupported field test."""
class FieldTestMethodMeta(type):
......@@ -35,6 +25,15 @@ class FieldTestMethodMeta(type):
type(cls).CLASSES.append(cls)
def print_called(f):
"""Decorator, prints a message when a function is called."""
def decorated(*args, **kwargs):
print(f'\nCalled: {f.__name__}')
return f(*args, **kwargs)
decorated.__name__ = f.__name__
return decorated
class FieldTestMethod(metaclass=FieldTestMethodMeta):
"""Field test method factory.
......@@ -45,16 +44,20 @@ class FieldTestMethod(metaclass=FieldTestMethodMeta):
name_formatter = None
@classmethod
def create(cls, attr, value):
def create(cls, field, attr, value):
if not cls.name_formatter:
raise ValueError(f'Name formatter not defined for {cls.__name__}')
test_method = cls.get_test_method(attr, value)
test_method = cls.get_test_method(field, attr, value)
test_method.__name__ = cls.name_formatter.format(
attr=attr, value=value)
if PRINT_FIELD_TEST_CALLS:
test_method = print_called(test_method)
return test_method
@classmethod
def get_test_method(cls, attr, value):
def get_test_method(cls, field, attr, value):
raise NotImplementedError
@classmethod
......@@ -87,14 +90,18 @@ def field_test_method(name_formatter=None, accept=None):
name_formatter = formatter
@classmethod
def get_test_method(cls, attr, value):
def get_test_method(cls, field_name, attr, value):
def test_method(self):
f(self, attr, value)
field = self.model._meta.get_field(field_name)
f(self, field, attr, value)
return test_method
@classmethod
def accepts(cls, attr, value):
return accept(attr, value)
acc = accept(attr, value)
# status = 'accepted' if acc else 'rejected'
# print(f'{cls.__name__} {status} {attr}')
return acc
MyFieldTestMethod.__name__ = FieldTestMethod.name_from_func(f)
......@@ -103,70 +110,87 @@ def field_test_method(name_formatter=None, accept=None):
@field_test_method(name_formatter='test_{attr}_is_{value}',
accept=lambda attr, value: isinstance(value, bool))
def test_bool(self, attr, value):
def test_bool(self, field, attr, value):
"""Test that a field attribute is True or False according to value."""
if value is True:
self.assertTrue(getattr(self.field, attr))
self.assertTrue(getattr(field, attr))
else:
self.assertFalse(getattr(self.field, attr))
self.assertFalse(getattr(field, attr))
@field_test_method(name_formatter='test_{attr}_value',
accept=lambda attr, value: isinstance(value, str))
def test_string_equals(self, attr, value):
def test_string_equals(self, field, attr, value):
"""Test that a field attribute is the given string value."""
self.assertEqual(getattr(self.field, attr), value)
self.assertEqual(getattr(field, attr), value)
class FieldTestCaseMeta(type):
"""Metaclass for FieldTestCase.
@field_test_method(name_formatter='test_{attr}_value',
accept=lambda attr, value: isinstance(value, Number))
def test_number_equals(self, field, attr, value):
"""Test that a field attribute is the given number value."""
self.assertEqual(getattr(field, attr), value)
Dynamically creates tests based on the class' tests dictionary.
@field_test_method(name_formatter='test_{attr}_choices',
accept=lambda attr, value: attr == 'choices')
def test_choices_equals(self, field, attr, value):
"""Test that a field choices are the given choice tuple."""
self.assertEqual(getattr(field, 'choices'), value)
class FieldTestMeta(type):
"""Abstract field test meta.
Manages the creation of field test methods.
"""
SUPPORTED_TESTS_DOCSTRING = """\n
Supported tests
---------------
Boolean value test:
'unique': True
String equality test:
'verbose_name': 'my-field-verbose-name'
Number equality test:
'max_length': 200
Field choices equality test:
'choices': (('C1', 'option 1'), ('C2', 'option 2'))
"""
def __new__(metacls, name, bases, namespace):
cls = super().__new__(metacls, name, bases, namespace)
cls.__doc__ += FieldTestMeta.SUPPORTED_TESTS_DOCSTRING
return cls
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
for attr, value in cls.tests.items():
added = type(cls).add_test_method(cls, attr, value)
if not added:
raise ValueError(f'Unsupported test: {attr}')
type(cls).before_dispatch(cls, name, bases, namespace)
type(cls).dispatch(cls)
def before_dispatch(cls, name, bases, namespace):
"""Callback called before the field test methods are created."""
pass
@classmethod
def add_test_method(metacls, cls, attr, value):
for test_method_cls in FieldTestMethod.all():
if test_method_cls.accepts(attr, value):
test_method = test_method_cls.create(attr, value)
setattr(cls, test_method.__name__, test_method)
return True
return False
class FieldTestCase(TestCase, metaclass=FieldTestCaseMeta):
"""Specialized test case for testing model fields.
Example
-------
class PhoneFieldTestCase(FieldTestCase):
model = PhoneNumber
field_name = 'phone_number'
tests = {
'verbose_name': 'numéro de téléphone',
'blank': True,
'unique': False,
}
"""
def dispatch_field(metacls, cls, tests, field_name):
"""Create test methods for a single field."""
for attr, value in tests.items():
type(cls).add_test_method(cls, field_name, attr, value)
model = None
field_name = None
tests = {}
@classmethod
def dispatch(metacls, cls):
raise NotImplementedError('Subclasses must implement dispatch()')
@classmethod
def setUpClass(cls):
super().setUpClass()
if not cls.model:
raise TypeError(f'Model not declared in {cls.__name__}')
if not cls.field_name:
raise TypeError(f'Field name not declared in {cls.__name__}')
def setUp(self):
self.field = self.model._meta.get_field(self.field_name)
def add_test_method(metacls, cls, field, attr, value):
test_method_cls = metacls.find_test_method(cls, attr, value)
test_method = test_method_cls.create(field, attr, value)
setattr(cls, test_method.__name__, test_method)
@classmethod
def find_test_method(metacls, cls, attr, value):
for test_method_cls in FieldTestMethod.all():
if test_method_cls.accepts(attr, value):
return test_method_cls
raise ValueError(f'Unsupported field test: {attr}')
"""Definition of FieldTestCase and ModelTestCase."""
from django.test import TestCase
from .field_test import FieldTestMeta
class FieldTestCaseMeta(FieldTestMeta):
"""Metaclass for FieldTestCase.
Dynamically creates tests based on the class' tests dictionary.
"""
@classmethod
def dispatch(metacls, cls):
metacls.dispatch_field(cls, cls.tests, cls.field_name)
class FieldTestCase(TestCase, metaclass=FieldTestCaseMeta):
"""Specialized test case for testing model fields.
Attributes
----------
model : django.db.Model
field_name : str
Name of the field to generate tests for.
tests : dict
Define the tested attributes and their expected values here.
Example usage
-------------
class PhoneFieldTestCase(FieldTestCase):
'''Test phone_number field of User.'''
model = User
field_name = 'phone_number'
tests = {
'verbose_name': 'numéro de téléphone',
'blank': True,
'unique': False,
}
"""
model = None
field_name = None
tests = {}
@classmethod
def setUpClass(cls):
super().setUpClass()
if not cls.model:
raise TypeError(f'Model not declared in {cls.__name__}')
if not cls.field_name:
raise TypeError(f'Field name not declared in {cls.__name__}')
class ModelTestCaseMeta(FieldTestMeta):
"""Metaclass for ModelTestCase.
Dynamically creates field tests based on the class' field_tests dictionary.
"""
@classmethod
def before_dispatch(metacls, cls, name, bases, namespace):
if not hasattr(cls, 'model'):
raise ValueError(f'Model not defined for {cls.__name__}')
if 'tests' in namespace and 'field_tests' not in namespace:
raise AttributeError(
'Model field tests should be defined in '
f'field_tests, not tests (in {cls.__name__})'
)
@classmethod
def dispatch(metacls, cls):
for field_name, tests in cls.field_tests.items():
super().dispatch_field(cls, tests, field_name)
class ModelTestCase(TestCase, metaclass=ModelTestCaseMeta):
"""Specialized test case for generic testing of models.
Attributes
----------
model : django.db.Model
field_tests : dict
Used to automatically generate field tests.
Must map a field name to a dictionary mapping tested field
attributes to their expected value.
Example usage
-------------
class UserModelTest(ModelTestCase):
'''Test the User model.'''
model = User
field_tests = {
'email': {'blank': False},
'phone_number': {'verbose_name': 'téléphone'},
}
"""
model = None
field_tests = {}
@classmethod
def setUpClass(cls):
super().setUpClass()
if not cls.model:
raise TypeError(f'Model not declared in {cls.__name__}')
......@@ -3,62 +3,12 @@ from django.test import TestCase
from django.db import IntegrityError
from django.contrib.auth import get_user_model
from tests.utils import random_email, FieldTestCase
from tests.utils import random_email, ModelTestCase
User = get_user_model()
class UserTest(TestCase):
"""Unit tests for the customized User model."""
@classmethod
def setUpTestData(self):
self.obj = User.objects.create(email=random_email())
# date_of_birth field
def test_date_of_birth_field_label(self):
self.assertEqual(User._meta.get_field('date_of_birth').verbose_name,
'date de naissance')
# gender field
def test_gender_field_label(self):
self.assertEqual(User._meta.get_field('gender').verbose_name, 'sexe')
def test_gender_choices(self):
self.assertTupleEqual(User._meta.get_field('gender').choices,
(('M', 'Homme'), ('F', 'Femme')))
def test_gender_max_length(self):
self.assertEqual(User._meta.get_field('gender').max_length, 1)
def test_gender_default(self):
field = User._meta.get_field('gender')
self.assertEqual(User.MALE, field.default)
self.assertFalse(field.null)
# phone_number field
def test_phone_number_field_label(self):
self.assertEqual(User._meta.get_field('phone_number').verbose_name,
'téléphone')
def test_phone_number_is_blank_and_nullable(self):
field = User._meta.get_field('phone_number')
self.assertTrue(field.blank)
self.assertTrue(field.null)
# url
def test_get_absolute_url(self):
response = self.client.get(f'/api/users/{self.obj.id}', follow=True)
self.assertEqual(200, response.status_code)
# TODO add phone number validation tests
class EmailAuthenticationTest(TestCase):
"""Tests to make sure a user can authenticate with email and password."""
......@@ -80,41 +30,52 @@ class EmailAuthenticationTest(TestCase):
self.assertFalse(logged_in)
class UserUsernameFieldTest(FieldTestCase):
"""Test the User.username field.
The email authentication system must have released the imperative
of having username unique and non-nullable.
"""
class UserModelTest(ModelTestCase):
"""Test the user model."""
model = User
field_name = 'username'
tests = {
field_tests = {
'username': {
'unique': False,
'blank': True,
'null': True,
},
'email': {
'unique': True,
'blank': False,
'null': False,
},
'date_of_birth': {
'verbose_name': 'date de naissance',
'blank': False,
'null': True,
},
'gender': {
'verbose_name': 'sexe',
'max_length': 1,
'default': User.MALE,
'choices': (('M', 'Homme'), ('F', 'Femme')),
'blank': False,
},
'phone_number': {
'verbose_name': 'téléphone',
'blank': True,
'null': True,
},
}
@classmethod
def setUpTestData(self):
self.obj = User.objects.create(email=random_email())
def test_get_absolute_url(self):
response = self.client.get(f'/api/users/{self.obj.id}', follow=True)
self.assertEqual(200, response.status_code)
def test_two_users_with_same_username_allowed(self):
self.model.objects.create(email=random_email())
self.model.objects.create(email=random_email())
class UserEmailFieldTest(FieldTestCase):
"""Test the User.email field.
The email authentication system must have made the email unique and
non-nullable from a database point of view.
"""
model = User
field_name = 'email'
tests = {
'unique': True,
'blank': False,
'null': False,
}
def test_two_users_with_same_email_not_allowed(self):
with self.assertRaises(IntegrityError):
self.model.objects.create(email='same.email@example.net')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment