From e858bb4a9bbcf89aa0031e1d76cbcb067f5b2ddf Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 11 Oct 2018 17:21:26 -0400 Subject: [PATCH 01/22] workaround "'PKOnlyObject' object has no attribute" error --- rest_framework_json_api/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index f8766bc4..2cae38d1 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -164,6 +164,12 @@ def get_related_instance(self): field = parent_serializer.fields.get(field_name, None) if field is not None: + # TODO: Workaround, not sure this is a correct fix. + # when many=False (a toOne relationship), must override field.use_pk_only_optimization() + # to return False as `related` needs the attributes + # and raises: `'PKOnlyObject' object has no attribute ''` otherwise. + if hasattr(field, 'use_pk_only_optimization') and field.use_pk_only_optimization(): + field.use_pk_only_optimization = lambda: False return field.get_attribute(parent_obj) else: try: From 088057535ea6684aa1d4153149006b13ffbf2e26 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 11:09:12 -0400 Subject: [PATCH 02/22] add new Course and Term models to reproduce "'PKOnlyObject' object has no attribute": https://github.com/django-json-api/django-rest-framework-json-api/issues/489 --- example/fixtures/drf_example.json | 422 +++++++++++++++++++++++++ example/migrations/0006_course_term.py | 52 +++ example/models.py | 55 ++++ example/serializers.py | 90 +++++- example/urls.py | 21 +- example/urls_test.py | 21 +- example/views.py | 38 ++- 7 files changed, 694 insertions(+), 5 deletions(-) create mode 100644 example/migrations/0006_course_term.py diff --git a/example/fixtures/drf_example.json b/example/fixtures/drf_example.json index 498c0d1c..3b97f6ed 100644 --- a/example/fixtures/drf_example.json +++ b/example/fixtures/drf_example.json @@ -120,5 +120,427 @@ "body": "Frist comment!!!", "author": null } +}, +{ + "model": "example.course", + "pk": "001b55e0-9a60-4386-98c7-4c856bb840b4", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "ANTB", + "course_number": "04961", + "course_identifier": "ANTH3160V", + "course_name": "THE BODY AND SOCIETY", + "course_description": "THE BODY AND SOCIETY" + } +}, +{ + "model": "example.course", + "pk": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73272", + "course_identifier": "ACCT8122B", + "course_name": "Accounting for Consultants", + "course_description": "Accounting for Consultants" + } +}, +{ + "model": "example.course", + "pk": "016659e9-e29f-49b4-b85d-d25da0724dbb", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73290", + "course_identifier": "ACCT7022B", + "course_name": "Accounting for Value", + "course_description": "Accounting for Value" + } +}, +{ + "model": "example.course", + "pk": "01ca197f-c00c-4f24-a743-091b62f1d500", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "AMSB", + "course_number": "00373", + "course_identifier": "AMST3704X", + "course_name": "SENIOR RESEARCH ESSAY SEMINAR", + "course_description": "SENIOR RESEARCH ESSAY SEMINAR" + } +}, +{ + "model": "example.course", + "pk": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "O", + "suffix_two": "00", + "subject_area_code": "AUDT", + "course_number": "87448", + "course_identifier": "AUPH1010O", + "course_name": "METHODS/PROB OF PHILOS THOUGHT", + "course_description": "METHODS/PROB OF PHILOS THOUGHT" + } +}, +{ + "model": "example.course", + "pk": "02e2e004-326e-4be8-aecc-aa67ece50fdf", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGF2U", + "suffix_two": "00", + "subject_area_code": "COMS", + "course_number": "84695", + "course_identifier": "COMS3102W", + "course_name": "DEVELOPMENT TECHNOLOGY", + "course_description": "MODERN iOS APPLICATION DEVELOP" + } +}, +{ + "model": "example.course", + "pk": "0381673f-e0a4-4212-b95a-3b62ebff9267", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "IG", + "suffix_two": "00", + "subject_area_code": "APAM", + "course_number": "74450", + "course_identifier": "APPH9143E", + "course_name": "APPLIED PHYSICS SEMINAR", + "course_description": "STELLARATOR PHYSICS" + } +}, +{ + "model": "example.course", + "pk": "03e32754-3da7-4005-be6b-8de0e088816a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEF", + "suffix_two": "00", + "subject_area_code": "CEEM", + "course_number": "26118", + "course_identifier": "CIEN3304E", + "course_name": "IND STUDIES-CIVIL ENGIN-SENIOR", + "course_description": "IND STUDIES-CIVIL ENGIN-SENIOR" + } +}, +{ + "model": "example.course", + "pk": "046741cd-c700-4752-b57a-e37a948ebc44", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "BUEC", + "course_number": "72074", + "course_identifier": "BUEC7255B", + "course_name": "FinTech: Consumer Financial Se", + "course_description": "FinTech: Consumer Financial Se" + } +}, +{ + "model": "example.course", + "pk": "04893b8f-0cbe-4e09-b8e6-17a4745900c1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGFKU", + "suffix_two": "00", + "subject_area_code": "ENCL", + "course_number": "89695", + "course_identifier": "CLEN4414W", + "course_name": "HIST OF LITERARY CRITICISM:PLATO TO KANT", + "course_description": "HIST OF LIT CRIT PLATO TO KANT" + } +}, +{ + "model": "example.term", + "pk": "00290ba0-ebae-44c0-9f4b-58a5f27240ed", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "00d14ddb-9fb5-4cff-9954-d52fc33217e7", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "010a7ff7-ef5a-4b36-b3ff-9c34e30b76e8", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "01163a94-fc8f-47fe-bb4a-5407ad1a35fe", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } +}, +{ + "model": "example.term", + "pk": "01764ebb-34b7-4b21-8835-cf712532cf5c", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20182", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "02e877b2-35c4-47d4-b72c-25bab1e87065", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "0316e6ad-fe6a-4339-8d18-a98e4ffb0bee", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "035c31c5-398d-43b7-a55b-19f6d1472797", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "0378c6c0-b658-4cf6-b8ba-6fa19614e3aa", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20192", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "243e2b9c-a3c6-4d40-9b9a-2750d6c03250", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "2d763c14-a566-4600-860f-329e44cbbd4a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "39ca7b38-f273-4fa3-9494-5a422780aebd", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "52cc86dd-7a78-48b8-a6a5-76c1fc7fc9be", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "964ff272-acb8-4adc-9a7e-21a241e63ff1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "bca761f7-03f6-4ff5-bbb8-b58467ef3970", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "e8e13192-0677-44e7-a590-606a38d66b34", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "f9057456-fed6-4982-bb82-3276999cb1ae", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "f9aa1a51-bf3b-45cf-b1cc-34ce47ca9913", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } } ] diff --git a/example/migrations/0006_course_term.py b/example/migrations/0006_course_term.py new file mode 100644 index 00000000..53496898 --- /dev/null +++ b/example/migrations/0006_course_term.py @@ -0,0 +1,52 @@ +# Generated by Django 2.1 on 2018-10-13 09:33 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('example', '0005_auto_20180922_1508'), + ] + + operations = [ + migrations.CreateModel( + name='Course', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('effective_start_date', models.DateField(blank=True, default=None, null=True)), + ('effective_end_date', models.DateField(blank=True, default=None, null=True)), + ('last_mod_user_name', models.CharField(max_length=80)), + ('last_mod_date', models.DateField(auto_now=True)), + ('school_bulletin_prefix_code', models.CharField(max_length=10)), + ('suffix_two', models.CharField(max_length=2)), + ('subject_area_code', models.CharField(max_length=10)), + ('course_number', models.CharField(max_length=10)), + ('course_identifier', models.CharField(max_length=10, unique=True)), + ('course_name', models.CharField(max_length=80)), + ('course_description', models.TextField()), + ], + options={ + 'ordering': ['course_number'], + }, + ), + migrations.CreateModel( + name='Term', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('effective_start_date', models.DateField(blank=True, default=None, null=True)), + ('effective_end_date', models.DateField(blank=True, default=None, null=True)), + ('last_mod_user_name', models.CharField(max_length=80)), + ('last_mod_date', models.DateField(auto_now=True)), + ('term_identifier', models.TextField(max_length=10)), + ('audit_permitted_code', models.PositiveIntegerField(blank=True, default=0)), + ('exam_credit_flag', models.BooleanField(default=True)), + ('course', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='course', to='example.Course')), + ], + options={ + 'ordering': ['term_identifier'], + }, + ), + ] diff --git a/example/models.py b/example/models.py index f183391e..0ac0ef06 100644 --- a/example/models.py +++ b/example/models.py @@ -7,6 +7,8 @@ from django.utils.encoding import python_2_unicode_compatible from polymorphic.models import PolymorphicModel +import uuid + class BaseModel(models.Model): """ @@ -152,3 +154,56 @@ class Company(models.Model): def __str__(self): return self.name + + +# the following serializers are to reproduce/confirm fix for this bug: +# https://github.com/django-json-api/django-rest-framework-json-api/issues/489 +class CommonModel(models.Model): + """ + Abstract model with common fields for all "real" Models: + - id: globally unique UUID version 4 + - effective dates + - last modified dates + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + effective_start_date = models.DateField(default=None, blank=True, null=True) + effective_end_date = models.DateField(default=None, blank=True, null=True) + last_mod_user_name = models.CharField(max_length=80) + last_mod_date = models.DateField(auto_now=True) + + class Meta: + abstract = True + + +class Course(CommonModel): + """ + A course of instruction. e.g. COMSW1002 Computing in Context + """ + school_bulletin_prefix_code = models.CharField(max_length=10) + suffix_two = models.CharField(max_length=2) + subject_area_code = models.CharField(max_length=10) + course_number = models.CharField(max_length=10) + course_identifier = models.CharField(max_length=10, unique=True) + course_name = models.CharField(max_length=80) + course_description = models.TextField() + + class Meta: + # verbose_name = "Course" + # verbose_name_plural = "Courses" + ordering = ["course_number"] + + +class Term(CommonModel): + """ + A specific course term (year+semester) instance. + e.g. 20183COMSW1002 + """ + term_identifier = models.TextField(max_length=10) + audit_permitted_code = models.PositiveIntegerField(blank=True, default=0) + exam_credit_flag = models.BooleanField(default=True) + course = models.ForeignKey('example.Course', related_name='terms', + on_delete=models.CASCADE, null=True, + default=None) + + class Meta: + ordering = ["term_identifier"] diff --git a/example/serializers.py b/example/serializers.py index 0cecad5d..8bcab089 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -17,7 +17,9 @@ Project, ProjectType, ResearchProject, - TaggedItem + TaggedItem, + Course, + Term, ) @@ -269,3 +271,89 @@ class Meta: model = Company if version.parse(rest_framework.VERSION) >= version.parse('3.3'): fields = '__all__' + + +# the following serializers are to reproduce/confirm fix for this bug: +# https://github.com/django-json-api/django-rest-framework-json-api/issues/489 +class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): + """ + .models.CommonModel.last_mod_user_name/date should come from auth.user on a POST/PATCH + """ + def _last_mod(self, validated_data): + """ + override any last_mod_user_name or date with current auth user and current date. + """ + validated_data['last_mod_user_name'] = self.context['request'].user + validated_data['last_mod_date'] = datetime.now().date() + + def create(self, validated_data): + """ + extended ModelSerializer to set last_mod_user/date + """ + self._last_mod(validated_data) + return super(HyperlinkedModelSerializer, self).create(validated_data) + + def update(self, validated_data): + """ + extended ModelSerializer to set last_mod_user/date + """ + self._last_mod(validated_data) + return super(HyperlinkedModelSerializer, self).update(validated_data) + + +class CourseSerializer(HyperlinkedModelSerializer): + """ + (de-)serialize the Course. + """ + terms = relations.ResourceRelatedField( + model=Term, + many=True, + read_only=False, + allow_null=True, + required=False, + queryset=Term.objects.all(), + self_link_view_name='course-relationships', + related_link_view_name='course-related', + ) + + # 'included' support (also used for `related_serializers` for DJA 2.6.0) + included_serializers = { + 'terms': 'example.serializers.TermSerializer', + } + + class Meta: + model = Course + fields = ( + 'url', + 'school_bulletin_prefix_code', 'suffix_two', 'subject_area_code', + 'course_number', 'course_identifier', 'course_name', 'course_description', + 'effective_start_date', 'effective_end_date', + 'last_mod_user_name', 'last_mod_date', + 'terms') + + +class TermSerializer(HyperlinkedModelSerializer): + course = relations.ResourceRelatedField( + model=Course, + many=False, # this breaks new 2.6.0 related support. Only works when True. + read_only=False, + allow_null=True, + required=False, + queryset=Course.objects.all(), + self_link_view_name='term-relationships', + related_link_view_name='term-related', + ) + + included_serializers = { + 'course': 'example.serializers.CourseSerializer', + } + + class Meta: + model = Term + fields = ( + 'url', + 'term_identifier', 'audit_permitted_code', + 'exam_credit_flag', + 'effective_start_date', 'effective_end_date', + 'last_mod_user_name', 'last_mod_date', + 'course') diff --git a/example/urls.py b/example/urls.py index 79d3b1c1..d7eeef4c 100644 --- a/example/urls.py +++ b/example/urls.py @@ -14,7 +14,11 @@ EntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + CourseViewSet, + TermViewSet, + CourseRelationshipView, + TermRelationshipView, ) router = routers.DefaultRouter(trailing_slash=False) @@ -27,6 +31,8 @@ router.register(r'companies', CompanyViewset) router.register(r'projects', ProjectViewset) router.register(r'project-types', ProjectTypeViewset) +router.register(r'courses', CourseViewSet) +router.register(r'terms', TermViewSet) urlpatterns = [ url(r'^', include(router.urls)), @@ -63,6 +69,19 @@ url(r'^authors/(?P[^/.]+)/relationships/(?P\w+)', AuthorRelationshipView.as_view(), name='author-relationships'), + + url(r'courses/(?P[^/.]+)/relationships/(?P\w+)', + CourseRelationshipView.as_view(), + name='course-relationships'), + url(r'courses/(?P[^/.]+)/(?P\w+)/$', + CourseViewSet.as_view({'get': 'retrieve_related'}), + name='course-related'), + url(r'terms/(?P[^/.]+)/relationships/(?P\w+)', + TermRelationshipView.as_view(), + name='term-relationships'), + url(r'terms/(?P[^/.]+)/(?P\w+)/$', + TermViewSet.as_view({'get': 'retrieve_related'}), + name='term-related'), ] diff --git a/example/urls_test.py b/example/urls_test.py index 94568ce4..35cd34fb 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -16,7 +16,11 @@ NoFiltersetEntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + CourseViewSet, + TermViewSet, + CourseRelationshipView, + TermRelationshipView, ) router = routers.DefaultRouter(trailing_slash=False) @@ -32,6 +36,8 @@ router.register(r'companies', CompanyViewset) router.register(r'projects', ProjectViewset) router.register(r'project-types', ProjectTypeViewset) +router.register(r'courses', CourseViewSet) +router.register(r'terms', TermViewSet) # for the old tests router.register(r'identities', Identity) @@ -79,4 +85,17 @@ url(r'^authors/(?P[^/.]+)/relationships/(?P\w+)', AuthorRelationshipView.as_view(), name='author-relationships'), + + url(r'courses/(?P[^/.]+)/relationships/(?P\w+)', + CourseRelationshipView.as_view(), + name='course-relationships'), + url(r'courses/(?P[^/.]+)/(?P\w+)/$', + CourseViewSet.as_view({'get': 'retrieve_related'}), + name='course-related'), + url(r'terms/(?P[^/.]+)/relationships/(?P\w+)', + TermRelationshipView.as_view(), + name='term-relationships'), + url(r'terms/(?P[^/.]+)/(?P\w+)/$', + TermViewSet.as_view({'get': 'retrieve_related'}), + name='term-related'), ] diff --git a/example/views.py b/example/views.py index aa0d67e7..5482287d 100644 --- a/example/views.py +++ b/example/views.py @@ -13,7 +13,17 @@ from rest_framework_json_api.utils import format_drf_errors from rest_framework_json_api.views import ModelViewSet, RelationshipView -from example.models import Author, Blog, Comment, Company, Entry, Project, ProjectType +from example.models import ( + Author, + Blog, + Comment, + Company, + Entry, + Project, + ProjectType, + Course, + Term, +) from example.serializers import ( AuthorSerializer, BlogSerializer, @@ -21,7 +31,9 @@ CompanySerializer, EntrySerializer, ProjectSerializer, - ProjectTypeSerializer + ProjectTypeSerializer, + CourseSerializer, + TermSerializer, ) HTTP_422_UNPROCESSABLE_ENTITY = 422 @@ -197,3 +209,25 @@ class CommentRelationshipView(RelationshipView): class AuthorRelationshipView(RelationshipView): queryset = Author.objects.all() self_link_view_name = 'author-relationships' + + +# the following views are to reproduce/confirm fix for this bug: +# https://github.com/django-json-api/django-rest-framework-json-api/issues/489 +class CourseViewSet(ModelViewSet): + queryset = Course.objects.all() + serializer_class = CourseSerializer + + +class TermViewSet(ModelViewSet): + queryset = Term.objects.all() + serializer_class = TermSerializer + + +class CourseRelationshipView(RelationshipView): + queryset = Course.objects + self_link_view_name = 'course-relationships' + + +class TermRelationshipView(RelationshipView): + queryset = Term.objects + self_link_view_name = 'term-relationships' From a948813cbb7ecebd134460e0df6f36157c54f17e Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 12:32:27 -0400 Subject: [PATCH 03/22] use RelatedMixin.override_pk_only_optimization=True to prevent AttributeError exception. --- example/fixtures/courseterm.json | 424 +++++++++++++++++++++++++++++++ example/tests/test_views.py | 28 +- rest_framework_json_api/views.py | 8 +- 3 files changed, 457 insertions(+), 3 deletions(-) create mode 100644 example/fixtures/courseterm.json diff --git a/example/fixtures/courseterm.json b/example/fixtures/courseterm.json new file mode 100644 index 00000000..4ec37109 --- /dev/null +++ b/example/fixtures/courseterm.json @@ -0,0 +1,424 @@ +[ +{ + "model": "example.course", + "pk": "001b55e0-9a60-4386-98c7-4c856bb840b4", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "ANTB", + "course_number": "04961", + "course_identifier": "ANTH3160V", + "course_name": "THE BODY AND SOCIETY", + "course_description": "THE BODY AND SOCIETY" + } +}, +{ + "model": "example.course", + "pk": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73272", + "course_identifier": "ACCT8122B", + "course_name": "Accounting for Consultants", + "course_description": "Accounting for Consultants" + } +}, +{ + "model": "example.course", + "pk": "016659e9-e29f-49b4-b85d-d25da0724dbb", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73290", + "course_identifier": "ACCT7022B", + "course_name": "Accounting for Value", + "course_description": "Accounting for Value" + } +}, +{ + "model": "example.course", + "pk": "01ca197f-c00c-4f24-a743-091b62f1d500", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "AMSB", + "course_number": "00373", + "course_identifier": "AMST3704X", + "course_name": "SENIOR RESEARCH ESSAY SEMINAR", + "course_description": "SENIOR RESEARCH ESSAY SEMINAR" + } +}, +{ + "model": "example.course", + "pk": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "O", + "suffix_two": "00", + "subject_area_code": "AUDT", + "course_number": "87448", + "course_identifier": "AUPH1010O", + "course_name": "METHODS/PROB OF PHILOS THOUGHT", + "course_description": "METHODS/PROB OF PHILOS THOUGHT" + } +}, +{ + "model": "example.course", + "pk": "02e2e004-326e-4be8-aecc-aa67ece50fdf", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGF2U", + "suffix_two": "00", + "subject_area_code": "COMS", + "course_number": "84695", + "course_identifier": "COMS3102W", + "course_name": "DEVELOPMENT TECHNOLOGY", + "course_description": "MODERN iOS APPLICATION DEVELOP" + } +}, +{ + "model": "example.course", + "pk": "0381673f-e0a4-4212-b95a-3b62ebff9267", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "IG", + "suffix_two": "00", + "subject_area_code": "APAM", + "course_number": "74450", + "course_identifier": "APPH9143E", + "course_name": "APPLIED PHYSICS SEMINAR", + "course_description": "STELLARATOR PHYSICS" + } +}, +{ + "model": "example.course", + "pk": "03e32754-3da7-4005-be6b-8de0e088816a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEF", + "suffix_two": "00", + "subject_area_code": "CEEM", + "course_number": "26118", + "course_identifier": "CIEN3304E", + "course_name": "IND STUDIES-CIVIL ENGIN-SENIOR", + "course_description": "IND STUDIES-CIVIL ENGIN-SENIOR" + } +}, +{ + "model": "example.course", + "pk": "046741cd-c700-4752-b57a-e37a948ebc44", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "BUEC", + "course_number": "72074", + "course_identifier": "BUEC7255B", + "course_name": "FinTech: Consumer Financial Se", + "course_description": "FinTech: Consumer Financial Se" + } +}, +{ + "model": "example.course", + "pk": "04893b8f-0cbe-4e09-b8e6-17a4745900c1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGFKU", + "suffix_two": "00", + "subject_area_code": "ENCL", + "course_number": "89695", + "course_identifier": "CLEN4414W", + "course_name": "HIST OF LITERARY CRITICISM:PLATO TO KANT", + "course_description": "HIST OF LIT CRIT PLATO TO KANT" + } +}, +{ + "model": "example.term", + "pk": "00290ba0-ebae-44c0-9f4b-58a5f27240ed", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "00d14ddb-9fb5-4cff-9954-d52fc33217e7", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "010a7ff7-ef5a-4b36-b3ff-9c34e30b76e8", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "01163a94-fc8f-47fe-bb4a-5407ad1a35fe", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } +}, +{ + "model": "example.term", + "pk": "01764ebb-34b7-4b21-8835-cf712532cf5c", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20182", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "02e877b2-35c4-47d4-b72c-25bab1e87065", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "0316e6ad-fe6a-4339-8d18-a98e4ffb0bee", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "035c31c5-398d-43b7-a55b-19f6d1472797", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "0378c6c0-b658-4cf6-b8ba-6fa19614e3aa", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20192", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "243e2b9c-a3c6-4d40-9b9a-2750d6c03250", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "2d763c14-a566-4600-860f-329e44cbbd4a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "39ca7b38-f273-4fa3-9494-5a422780aebd", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "52cc86dd-7a78-48b8-a6a5-76c1fc7fc9be", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "964ff272-acb8-4adc-9a7e-21a241e63ff1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "bca761f7-03f6-4ff5-bbb8-b58467ef3970", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "e8e13192-0677-44e7-a590-606a38d66b34", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "f9057456-fed6-4982-bb82-3276999cb1ae", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "f9aa1a51-bf3b-45cf-b1cc-34ce47ca9913", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } +} +] diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 48e1bfa6..82be5fb3 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,4 +1,5 @@ import json +import mock from django.test import RequestFactory from django.utils import timezone @@ -12,7 +13,7 @@ from . import TestBase from .. import views from example.factories import AuthorFactory, EntryFactory -from example.models import Author, Blog, Comment, Entry +from example.models import Author, Blog, Comment, Entry, Course, Term from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer from example.views import AuthorViewSet @@ -231,9 +232,12 @@ def test_delete_to_many_relationship_with_change(self): class TestRelatedMixin(APITestCase): + fixtures = ('courseterm',) def setUp(self): self.author = AuthorFactory() + self.course = Course.objects.all() + self.term = Term.objects.all() def _get_view(self, kwargs): factory = APIRequestFactory() @@ -319,6 +323,28 @@ def test_retrieve_related_None(self): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json(), {'data': None}) + # the following tests are to reproduce/confirm fix for this bug: + # https://github.com/django-json-api/django-rest-framework-json-api/issues/489 + def test_term_related_course(self): + """ + confirm that the related data reference the primary key + """ + term_id = self.term.first().pk + kwargs = {'pk': term_id, 'related_field': 'course'} + url = reverse('term-related', kwargs=kwargs) + with mock.patch('rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + True): + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + dja_response = resp.json() + back_reference = dja_response['data']['relationships']['terms']['data'] + self.assertIn({"type": "terms", "id": str(term_id)}, back_reference) + + # the following raises AttributeError: + with mock.patch('rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + False): + resp = self.client.get(url) + class TestValidationErrorResponses(TestBase): def test_if_returns_error_on_empty_post(self): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 2cae38d1..1dfad7dd 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -107,6 +107,9 @@ class RelatedMixin(object): """ This mixin handles all related entities, whose Serializers are declared in "related_serializers" """ + # test bug fix for https://github.com/django-json-api/django-rest-framework-json-api/issues/489 + #: override pk_only optimization + override_pk_only_optimization = False def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} @@ -168,8 +171,9 @@ def get_related_instance(self): # when many=False (a toOne relationship), must override field.use_pk_only_optimization() # to return False as `related` needs the attributes # and raises: `'PKOnlyObject' object has no attribute ''` otherwise. - if hasattr(field, 'use_pk_only_optimization') and field.use_pk_only_optimization(): - field.use_pk_only_optimization = lambda: False + if self.override_pk_only_optimization: + if hasattr(field, 'use_pk_only_optimization') and field.use_pk_only_optimization(): + field.use_pk_only_optimization = lambda: False return field.get_attribute(parent_obj) else: try: From 8c78bc775e1a3238a6e7dd61c171034abb51fdae Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 13:02:10 -0400 Subject: [PATCH 04/22] default override_pk_only_optimization True --- rest_framework_json_api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 1dfad7dd..361e82f6 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -109,7 +109,7 @@ class RelatedMixin(object): """ # test bug fix for https://github.com/django-json-api/django-rest-framework-json-api/issues/489 #: override pk_only optimization - override_pk_only_optimization = False + override_pk_only_optimization = True def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} From d645f8181496cd4fe2f0f9b5679e0dbf9edac9eb Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 13:03:59 -0400 Subject: [PATCH 05/22] tox, flake8, isort, make test exception a passing test --- example/models.py | 4 ++-- example/serializers.py | 4 ++-- example/tests/test_views.py | 22 +++++++++++++++------- example/urls.py | 6 +++--- example/urls_test.py | 6 +++--- example/views.py | 16 +++------------- 6 files changed, 28 insertions(+), 30 deletions(-) diff --git a/example/models.py b/example/models.py index 0ac0ef06..9dea768e 100644 --- a/example/models.py +++ b/example/models.py @@ -1,14 +1,14 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals +import uuid + from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.encoding import python_2_unicode_compatible from polymorphic.models import PolymorphicModel -import uuid - class BaseModel(models.Model): """ diff --git a/example/serializers.py b/example/serializers.py index 8bcab089..3a7ef88f 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -13,13 +13,13 @@ Blog, Comment, Company, + Course, Entry, Project, ProjectType, ResearchProject, TaggedItem, - Course, - Term, + Term ) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 82be5fb3..49e00507 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,5 +1,4 @@ import json -import mock from django.test import RequestFactory from django.utils import timezone @@ -13,10 +12,15 @@ from . import TestBase from .. import views from example.factories import AuthorFactory, EntryFactory -from example.models import Author, Blog, Comment, Entry, Course, Term +from example.models import Author, Blog, Comment, Course, Entry, Term from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer from example.views import AuthorViewSet +try: + from unittest import mock +except ImportError: + import mock + class TestRelationshipView(APITestCase): def setUp(self): @@ -323,11 +327,11 @@ def test_retrieve_related_None(self): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json(), {'data': None}) - # the following tests are to reproduce/confirm fix for this bug: + # the following test reproduces/confirm fix for this bug: # https://github.com/django-json-api/django-rest-framework-json-api/issues/489 def test_term_related_course(self): """ - confirm that the related data reference the primary key + confirm that the related child data reference the parent """ term_id = self.term.first().pk kwargs = {'pk': term_id, 'related_field': 'course'} @@ -341,9 +345,13 @@ def test_term_related_course(self): self.assertIn({"type": "terms", "id": str(term_id)}, back_reference) # the following raises AttributeError: - with mock.patch('rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', - False): - resp = self.client.get(url) + with self.assertRaises(AttributeError) as ae: + with mock.patch( + 'rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + False): + resp = self.client.get(url) + print(ae.exception) + self.assertIn('`PKOnlyObject`', ae.exception.args[0]) class TestValidationErrorResponses(TestBase): diff --git a/example/urls.py b/example/urls.py index d7eeef4c..65162b73 100644 --- a/example/urls.py +++ b/example/urls.py @@ -10,15 +10,15 @@ CommentRelationshipView, CommentViewSet, CompanyViewset, + CourseRelationshipView, + CourseViewSet, EntryRelationshipView, EntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, ProjectViewset, - CourseViewSet, - TermViewSet, - CourseRelationshipView, TermRelationshipView, + TermViewSet ) router = routers.DefaultRouter(trailing_slash=False) diff --git a/example/urls_test.py b/example/urls_test.py index 35cd34fb..54063aa3 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -10,6 +10,8 @@ CommentRelationshipView, CommentViewSet, CompanyViewset, + CourseRelationshipView, + CourseViewSet, EntryRelationshipView, EntryViewSet, FiltersetEntryViewSet, @@ -17,10 +19,8 @@ NonPaginatedEntryViewSet, ProjectTypeViewset, ProjectViewset, - CourseViewSet, - TermViewSet, - CourseRelationshipView, TermRelationshipView, + TermViewSet ) router = routers.DefaultRouter(trailing_slash=False) diff --git a/example/views.py b/example/views.py index 5482287d..7940caa8 100644 --- a/example/views.py +++ b/example/views.py @@ -13,27 +13,17 @@ from rest_framework_json_api.utils import format_drf_errors from rest_framework_json_api.views import ModelViewSet, RelationshipView -from example.models import ( - Author, - Blog, - Comment, - Company, - Entry, - Project, - ProjectType, - Course, - Term, -) +from example.models import Author, Blog, Comment, Company, Course, Entry, Project, ProjectType, Term from example.serializers import ( AuthorSerializer, BlogSerializer, CommentSerializer, CompanySerializer, + CourseSerializer, EntrySerializer, ProjectSerializer, ProjectTypeSerializer, - CourseSerializer, - TermSerializer, + TermSerializer ) HTTP_422_UNPROCESSABLE_ENTITY = 422 From d0debda7964dab3fe098f10e0c46eb941ea6ba4f Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 23 Oct 2018 11:16:51 -0400 Subject: [PATCH 06/22] add drf39 and drfmaster tests (#498) --- .travis.yml | 50 ++++++++++++++++++++++++++++++++++++++++- CHANGELOG.md | 3 +++ README.rst | 16 ++++++++----- docs/getting-started.md | 6 ++--- tox.ini | 11 ++++++--- 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index ae4cb4de..c76e079e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,17 @@ language: python -sudo: false +sudo: required cache: pip # Favor explicit over implicit and use an explicit build matrix. matrix: + allow_failures: + - env: TOXENV=py34-df20-django20-drfmaster + - env: TOXENV=py35-df20-django20-drfmaster + - env: TOXENV=py36-df20-django20-drfmaster + - env: TOXENV=py37-df20-django20-drfmaster + - env: TOXENV=py35-df20-django21-drfmaster + - env: TOXENV=py36-df20-django21-drfmaster + - env: TOXENV=py37-df20-django21-drfmaster + include: - python: 3.6 env: TOXENV=flake8 @@ -13,6 +22,8 @@ matrix: env: TOXENV=py27-df11-django111-drf37 - python: 2.7 env: TOXENV=py27-df11-django111-drf38 + - python: 2.7 + env: TOXENV=py27-df11-django111-drf39 - python: 3.4 env: TOXENV=py34-df20-django111-drf36 @@ -24,6 +35,10 @@ matrix: env: TOXENV=py34-df20-django20-drf37 - python: 3.4 env: TOXENV=py34-df20-django20-drf38 + - python: 3.4 + env: TOXENV=py34-df20-django20-drf39 + - python: 3.4 + env: TOXENV=py34-df20-django20-drfmaster - python: 3.5 env: TOXENV=py35-df20-django111-drf36 @@ -35,6 +50,14 @@ matrix: env: TOXENV=py35-df20-django20-drf37 - python: 3.5 env: TOXENV=py35-df20-django20-drf38 + - python: 3.5 + env: TOXENV=py35-df20-django20-drf39 + - python: 3.5 + env: TOXENV=py35-df20-django20-drfmaster + - python: 3.5 + env: TOXENV=py35-df20-django21-drf39 + - python: 3.5 + env: TOXENV=py35-df20-django21-drfmaster - python: 3.6 env: TOXENV=py36-df20-django111-drf36 @@ -46,6 +69,31 @@ matrix: env: TOXENV=py36-df20-django20-drf37 - python: 3.6 env: TOXENV=py36-df20-django20-drf38 + - python: 3.6 + env: TOXENV=py36-df20-django20-drf39 + - python: 3.6 + env: TOXENV=py36-df20-django20-drfmaster + - python: 3.6 + env: TOXENV=py36-df20-django21-drf39 + - python: 3.6 + env: TOXENV=py36-df20-django21-drfmaster + + - python: 3.7 + dist: xenial + sudo: required + env: TOXENV=py37-df20-django20-drf39 + - python: 3.7 + dist: xenial + sudo: required + env: TOXENV=py37-df20-django20-drfmaster + - python: 3.7 + dist: xenial + sudo: required + env: TOXENV=py37-df20-django21-drf39 + - python: 3.7 + dist: xenial + sudo: required + env: TOXENV=py37-df20-django21-drfmaster install: - pip install tox script: diff --git a/CHANGELOG.md b/CHANGELOG.md index d4db4bbc..49114a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ any parts of the framework not mentioned in the documentation should generally b ### Added +* Add support for Django 2.1, DRF 3.9 and Python 3.7. Please note: + - Django >= 2.1 is not supported with Python < 3.5. + ### Deprecated ### Changed diff --git a/README.rst b/README.rst index 13579a24..e5b9f930 100644 --- a/README.rst +++ b/README.rst @@ -87,9 +87,9 @@ As a Django REST Framework JSON API (short DJA) we are trying to address followi Requirements ------------ -1. Python (2.7, 3.4, 3.5, 3.6) -2. Django (1.11, 2.0) -3. Django REST Framework (3.6, 3.7, 3.8) +1. Python (2.7, 3.4, 3.5, 3.6, 3.7) +2. Django (1.11, 2.0, 2.1) +3. Django REST Framework (3.6, 3.7, 3.8, 3.9) ------------ Installation @@ -116,12 +116,18 @@ From Source Running the example app ^^^^^^^^^^^^^^^^^^^^^^^ +It is recommended to create a virtualenv for testing. Assuming it is already +installed and activated: + :: $ git clone https://github.com/django-json-api/django-rest-framework-json-api.git $ cd django-rest-framework-json-api + $ pip install -r example/requirements.txt $ pip install -e . - $ django-admin.py runserver --settings=example.settings + $ django-admin migrate --settings=example.settings + $ django-admin loaddata drf_example --settings=example.settings + $ django-admin runserver --settings=example.settings Browse to http://localhost:8000 @@ -136,7 +142,7 @@ installed and activated: $ pip install -r requirements-development.txt $ flake8 - $ py.test + $ DJANGO_SETTINGS_MODULE=example.settings.test py.test ----- Usage diff --git a/docs/getting-started.md b/docs/getting-started.md index 26117e0b..baa53189 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -51,9 +51,9 @@ like the following: ## Requirements -1. Python (2.7, 3.4, 3.5, 3.6) -2. Django (1.11, 2.0) -3. Django REST Framework (3.6, 3.7, 3.8) +1. Python (2.7, 3.4, 3.5, 3.6, 3.7) +2. Django (1.11, 2.0, 2.1) +3. Django REST Framework (3.6, 3.7, 3.8, 3.9) ## Installation diff --git a/tox.ini b/tox.ini index c4812e0b..30f6b86f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,21 @@ [tox] envlist = - py27-df11-django111-drf{36,37,38} - py{34,35,36}-df20-django111-drf{36,37,38}, - py{34,35,36}-df20-django20-drf{37,38}, + py27-df11-django111-drf{36,37,38,39} + py{34,35,36}-df20-django111-drf{36,37,38,39,master}, + py{34,35,36}-df20-django20-drf{37,38,39,master}, + py37-df20-django20-drf{39,master}, + py{35,36,37}-df20-django21-drf{39,master}, [testenv] deps = django111: Django>=1.11,<1.12 django20: Django>=2.0,<2.1 + django21: Django>=2.1,<2.2 drf36: djangorestframework>=3.6.3,<3.7 drf37: djangorestframework>=3.7.0,<3.8 drf38: djangorestframework>=3.8.0,<3.9 + drf39: djangorestframework>=3.9.0,<3.10 + drfmaster: https://github.com/encode/django-rest-framework/archive/master.zip df11: django-filter<=1.1 df20: django-filter>=2.0 From 00fb5dcbfdc7dc48998373a1204e40a9c6e474d5 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Wed, 24 Oct 2018 19:55:57 +0200 Subject: [PATCH 07/22] Adjust to new flake8 3.6.0 version (#501) --- requirements-development.txt | 2 +- rest_framework_json_api/relations.py | 14 ++++++-------- setup.cfg | 12 +++++++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/requirements-development.txt b/requirements-development.txt index 834dc094..807c78a1 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -4,7 +4,7 @@ django-filter>=2.0 django-polymorphic>=2.0 factory-boy Faker -flake8 +flake8==3.6.0 flake8-isort isort mock diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 044b6f9f..82b94cd5 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -116,14 +116,12 @@ def get_links(self, obj=None, lookup_field='pk'): }) self_link = self.get_url('self', self.self_link_view_name, self_kwargs, request) - """ - Assuming RelatedField will be declared in two ways: - 1. url(r'^authors/(?P[^/.]+)/(?P\w+)/$', - AuthorViewSet.as_view({'get': 'retrieve_related'})) - 2. url(r'^authors/(?P[^/.]+)/bio/$', - AuthorBioViewSet.as_view({'get': 'retrieve'})) - So, if related_link_url_kwarg == 'pk' it will add 'related_field' parameter to reverse() - """ + # Assuming RelatedField will be declared in two ways: + # 1. url(r'^authors/(?P[^/.]+)/(?P\w+)/$', + # AuthorViewSet.as_view({'get': 'retrieve_related'})) + # 2. url(r'^authors/(?P[^/.]+)/bio/$', + # AuthorBioViewSet.as_view({'get': 'retrieve'})) + # So, if related_link_url_kwarg == 'pk' it will add 'related_field' parameter to reverse() if self.related_link_url_kwarg == 'pk': related_kwargs = self_kwargs else: diff --git a/setup.cfg b/setup.cfg index dd743ab2..effb04ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,12 +5,13 @@ test = pytest universal = 1 [flake8] -ignore = F405 +ignore = F405,W504 max-line-length = 100 exclude = - docs/conf.py, build, + docs/conf.py, migrations, + .eggs .tox, [isort] @@ -21,7 +22,12 @@ known_localfolder = example known_standard_library = mock line_length = 100 multi_line_output = 3 -skip=migrations,.tox,docs/conf.py +skip= + build, + docs/conf.py, + migrations, + .eggs + .tox, [coverage:report] omit= From b3eed32c50dedf4cc8d405f7e4fd92a2659a70dd Mon Sep 17 00:00:00 2001 From: Mohammed Ali Zubair Date: Fri, 26 Oct 2018 00:22:59 +0600 Subject: [PATCH 08/22] Avoid patch on `RelationshipView` deleting relationship instance when constraint would allow null (#499) --- CHANGELOG.md | 1 + example/tests/test_views.py | 46 +++++++++++++++++++++++++++++++- rest_framework_json_api/views.py | 23 +++++++++++++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49114a36..c60329ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ any parts of the framework not mentioned in the documentation should generally b ### Fixed * Pass context from `PolymorphicModelSerializer` to child serializers to support fields which require a `request` context such as `url`. +* Avoid patch on `RelationshipView` deleting relationship instance when constraint would allow null ([#242](https://github.com/django-json-api/django-rest-framework-json-api/issues/242)) ## [2.6.0] - 2018-09-20 diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 48e1bfa6..5b778b90 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -11,7 +11,7 @@ from . import TestBase from .. import views -from example.factories import AuthorFactory, EntryFactory +from example.factories import AuthorFactory, CommentFactory, EntryFactory from example.models import Author, Blog, Comment, Entry from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer from example.views import AuthorViewSet @@ -229,6 +229,50 @@ def test_delete_to_many_relationship_with_change(self): response = self.client.delete(url, data=request_data) assert response.status_code == 200, response.content.decode() + def test_new_comment_data_patch_to_many_relationship(self): + entry = EntryFactory(blog=self.blog, authors=(self.author,)) + comment = CommentFactory(entry=entry) + + url = '/authors/{}/relationships/comment_set'.format(self.author.id) + request_data = { + 'data': [{'type': format_resource_type('Comment'), 'id': str(comment.id)}, ] + } + previous_response = { + 'data': [ + {'type': 'comments', + 'id': str(self.second_comment.id) + } + ], + 'links': { + 'self': 'http://testserver/authors/{}/relationships/comment_set'.format( + self.author.id + ) + } + } + + response = self.client.get(url) + assert response.status_code == 200 + assert response.json() == previous_response + + new_patched_response = { + 'data': [ + {'type': 'comments', + 'id': str(comment.id) + } + ], + 'links': { + 'self': 'http://testserver/authors/{}/relationships/comment_set'.format( + self.author.id + ) + } + } + + response = self.client.patch(url, data=request_data) + assert response.status_code == 200 + assert response.json() == new_patched_response + + assert Comment.objects.filter(id=self.second_comment.id).exists() + class TestRelatedMixin(APITestCase): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index f8766bc4..9f3178a1 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -251,6 +251,18 @@ def get(self, request, *args, **kwargs): serializer_instance = self._instantiate_serializer(related_instance) return Response(serializer_instance.data) + def remove_relationships(self, instance_manager, field): + field_object = getattr(instance_manager, field) + + if field_object.null: + for obj in instance_manager.all(): + setattr(obj, field_object.name, None) + obj.save() + else: + instance_manager.all().delete() + + return instance_manager + def patch(self, request, *args, **kwargs): parent_obj = self.get_object() related_instance_or_manager = self.get_related_instance() @@ -261,7 +273,16 @@ def patch(self, request, *args, **kwargs): data=request.data, model_class=related_model_class, many=True ) serializer.is_valid(raise_exception=True) - related_instance_or_manager.all().delete() + + # for to one + if hasattr(related_instance_or_manager, "field"): + related_instance_or_manager = self.remove_relationships( + instance_manager=related_instance_or_manager, field="field") + # for to many + else: + related_instance_or_manager = self.remove_relationships( + instance_manager=related_instance_or_manager, field="target_field") + # have to set bulk to False since data isn't saved yet class_name = related_instance_or_manager.__class__.__name__ if class_name != 'ManyRelatedManager': From 860a0961143f10a4888a97a7937582c8854bac9b Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Thu, 25 Oct 2018 21:21:56 +0200 Subject: [PATCH 09/22] Use pytest instead of py.test (#502) py.test is the old way of running pytest. Also remove DJANGO_SETTINGS_MODULE env when running tests This is already defined in pytest.ini and py.test will take it from as it should also take parameters we might add in pytest.ini in the future. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e5b9f930..61ea29c2 100644 --- a/README.rst +++ b/README.rst @@ -142,7 +142,7 @@ installed and activated: $ pip install -r requirements-development.txt $ flake8 - $ DJANGO_SETTINGS_MODULE=example.settings.test py.test + $ pytest ----- Usage From a4d063f61c022e0923a83f2500a39d5cd97e7702 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Thu, 25 Oct 2018 21:51:33 +0200 Subject: [PATCH 10/22] Use pyupio to manage development dependencies (#503) This way CI doesn't suddently break when a dependency has updated but we still keep up-to-date. pyupio will open a PR with the updated dependencies. --- .pyup.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .pyup.yml diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 00000000..02f8ed99 --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,5 @@ +search: False +requirements: + - requirements-development.txt: + update: all + pin: True From 56b5fd733ee60d94a155d3508cfadea62ac99093 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 26 Oct 2018 01:16:01 -0700 Subject: [PATCH 11/22] Initial Update (#506) --- requirements-development.txt | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/requirements-development.txt b/requirements-development.txt index 807c78a1..c4a90dee 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -1,17 +1,17 @@ -e . -django-debug-toolbar -django-filter>=2.0 -django-polymorphic>=2.0 -factory-boy -Faker +django-debug-toolbar==1.10.1 +django-filter==2.0.0 +django-polymorphic==2.0.3 +factory-boy==2.11.1 +Faker==0.9.2 flake8==3.6.0 -flake8-isort -isort -mock -pytest -pytest-django -pytest-factoryboy -recommonmark -Sphinx -sphinx_rtd_theme -twine +flake8-isort==2.5 +isort==4.3.4 +mock==2.0.0 +pytest==3.9.2 +pytest-django==3.4.3 +pytest-factoryboy==2.0.1 +recommonmark==0.4.0 +Sphinx==1.8.1 +sphinx_rtd_theme==0.4.2 +twine==1.12.1 From 1ce84f83dd22ac180515671be0cbe5919e1b21a1 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Fri, 26 Oct 2018 15:55:10 +0200 Subject: [PATCH 12/22] Adjust depreaction warning to state to move to JSON_API_FORMAT_FIELD_NAMES (#507) Changelog entry was correct so not adding a new one to add only noise. --- example/settings/test.py | 2 +- rest_framework_json_api/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/settings/test.py b/example/settings/test.py index c165e187..c32aa95f 100644 --- a/example/settings/test.py +++ b/example/settings/test.py @@ -9,7 +9,7 @@ ROOT_URLCONF = 'example.urls_test' -JSON_API_FIELD_NAMES = 'camelize' +JSON_API_FORMAT_FIELD_NAMES = 'camelize' JSON_API_FORMAT_TYPES = 'camelize' JSON_API_PLURALIZE_TYPES = True diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 033a5730..19233132 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -133,7 +133,7 @@ def format_keys(obj, format_type=None): `format_keys` function and `JSON_API_FORMAT_KEYS` setting are deprecated and will be removed in the future. - Use `format_field_names` and `JSON_API_FIELD_NAMES` instead. Be aware that + Use `format_field_names` and `JSON_API_FORMAT_FIELD_NAMES` instead. Be aware that `format_field_names` only formats keys and preserves value. Takes either a dict or list and returns it with camelized keys only if @@ -144,7 +144,7 @@ def format_keys(obj, format_type=None): warnings.warn( "`format_keys` function and `JSON_API_FORMAT_KEYS` setting are deprecated and will be " "removed in the future. " - "Use `format_field_names` and `JSON_API_FIELD_NAMES` instead. Be aware that " + "Use `format_field_names` and `JSON_API_FORMAT_FIELD_NAMES` instead. Be aware that " "`format_field_names` only formats keys and preserves value.", DeprecationWarning ) From 7d1453750213becfc2c4d06a4bd4dabc5f77846d Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 29 Oct 2018 07:23:15 -0700 Subject: [PATCH 13/22] Update pytest from 3.9.2 to 3.9.3 (#508) --- requirements-development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-development.txt b/requirements-development.txt index c4a90dee..053a7e21 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -8,7 +8,7 @@ flake8==3.6.0 flake8-isort==2.5 isort==4.3.4 mock==2.0.0 -pytest==3.9.2 +pytest==3.9.3 pytest-django==3.4.3 pytest-factoryboy==2.0.1 recommonmark==0.4.0 From 274cb793a9011a4ad179d23d55b70181433164eb Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 11 Nov 2018 08:54:20 -0800 Subject: [PATCH 14/22] Update sphinx from 1.8.1 to 1.8.2 (#511) --- requirements-development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-development.txt b/requirements-development.txt index 053a7e21..67613c74 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -12,6 +12,6 @@ pytest==3.9.3 pytest-django==3.4.3 pytest-factoryboy==2.0.1 recommonmark==0.4.0 -Sphinx==1.8.1 +Sphinx==1.8.2 sphinx_rtd_theme==0.4.2 twine==1.12.1 From 0f09a6e4962874b11a5ecdc0428b51ab5c75cc02 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 11 Nov 2018 09:52:07 -0800 Subject: [PATCH 15/22] Update pytest-factoryboy from 2.0.1 to 2.0.2 (#510) --- requirements-development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-development.txt b/requirements-development.txt index 67613c74..23a73db9 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -10,7 +10,7 @@ isort==4.3.4 mock==2.0.0 pytest==3.9.3 pytest-django==3.4.3 -pytest-factoryboy==2.0.1 +pytest-factoryboy==2.0.2 recommonmark==0.4.0 Sphinx==1.8.2 sphinx_rtd_theme==0.4.2 From 7cdcff764ed259825e6f68e575809eba7f00f0d9 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 12 Nov 2018 01:50:03 -0800 Subject: [PATCH 16/22] Update pytest from 3.9.3 to 3.10.1 (#512) --- requirements-development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-development.txt b/requirements-development.txt index 23a73db9..9a96f4f1 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -8,7 +8,7 @@ flake8==3.6.0 flake8-isort==2.5 isort==4.3.4 mock==2.0.0 -pytest==3.9.3 +pytest==3.10.1 pytest-django==3.4.3 pytest-factoryboy==2.0.2 recommonmark==0.4.0 From 7ec8e422a146a5242999114d6ff624c9ba44c47d Mon Sep 17 00:00:00 2001 From: Mohammed Ali Zubair Date: Mon, 12 Nov 2018 20:09:19 +0600 Subject: [PATCH 17/22] Test support of DRF HyperlinkedIdentityField (#497) --- example/serializers.py | 17 +++++++- .../unit/test_default_drf_serializers.py | 42 +++++++++++++++++++ example/urls_test.py | 7 +++- example/views.py | 15 +++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 1fee79c4..c52f575f 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -63,7 +63,7 @@ class BlogDRFSerializer(drf_serilazers.ModelSerializer): DRF default serializer to test default DRF functionalities """ copyright = serializers.SerializerMethodField() - tags = TaggedItemSerializer(many=True, read_only=True) + tags = TaggedItemDRFSerializer(many=True, read_only=True) def get_copyright(self, resource): return datetime.now().year @@ -173,6 +173,21 @@ class JSONAPIMeta: included_resources = ['comments'] +class EntryDRFSerializers(drf_serilazers.ModelSerializer): + + tags = TaggedItemDRFSerializer(many=True, read_only=True) + url = drf_serilazers.HyperlinkedIdentityField( + view_name='drf-entry-blog-detail', + lookup_url_kwarg='entry_pk', + read_only=True, + ) + + class Meta: + model = Entry + fields = ('tags', 'url',) + read_only_fields = ('tags',) + + class AuthorTypeSerializer(serializers.ModelSerializer): class Meta: model = AuthorType diff --git a/example/tests/unit/test_default_drf_serializers.py b/example/tests/unit/test_default_drf_serializers.py index d09d293f..e17b9a52 100644 --- a/example/tests/unit/test_default_drf_serializers.py +++ b/example/tests/unit/test_default_drf_serializers.py @@ -184,3 +184,45 @@ def test_get_object_deletes_correct_blog(client, entry): resp = client.delete(url) assert resp.status_code == 204 + + +@pytest.mark.django_db +def test_get_entry_list_with_blogs(client, entry): + url = reverse('drf-entry-suggested', kwargs={'entry_pk': entry.id}) + resp = client.get(url) + + got = resp.json() + + expected = { + 'links': { + 'first': 'http://testserver/drf-entries/1/suggested/?page=1', + 'last': 'http://testserver/drf-entries/1/suggested/?page=1', + 'next': None, + 'prev': None + }, + 'data': [ + { + 'type': 'entries', + 'id': '1', + 'attributes': {}, + 'relationships': { + 'tags': { + 'data': [] + } + }, + 'links': { + 'self': 'http://testserver/drf-blogs/1' + } + } + ], + 'meta': { + 'pagination': { + 'page': 1, + 'pages': 1, + 'count': 1 + } + } + } + + assert resp.status_code == 200 + assert got == expected diff --git a/example/urls_test.py b/example/urls_test.py index 2e7d2d64..e51121ac 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -11,6 +11,7 @@ CommentViewSet, CompanyViewset, DRFBlogViewSet, + DRFEntryViewSet, EntryRelationshipView, EntryViewSet, FiltersetEntryViewSet, @@ -23,7 +24,7 @@ router = routers.DefaultRouter(trailing_slash=False) router.register(r'blogs', BlogViewSet) -# router to test default DRF functionalities +# router to test default DRF blog functionalities router.register(r'drf-blogs', DRFBlogViewSet, 'drf-entry-blog') router.register(r'entries', EntryViewSet) # these "flavors" of entries are used for various tests: @@ -59,6 +60,10 @@ EntryViewSet.as_view({'get': 'list'}), name='entry-suggested' ), + url(r'^drf-entries/(?P[^/.]+)/suggested/', + DRFEntryViewSet.as_view({'get': 'list'}), + name='drf-entry-suggested' + ), url(r'entries/(?P[^/.]+)/authors', AuthorViewSet.as_view({'get': 'list'}), name='entry-authors'), diff --git a/example/views.py b/example/views.py index 78cdc6ad..41036fc6 100644 --- a/example/views.py +++ b/example/views.py @@ -21,6 +21,7 @@ BlogSerializer, CommentSerializer, CompanySerializer, + EntryDRFSerializers, EntrySerializer, ProjectSerializer, ProjectTypeSerializer @@ -104,6 +105,20 @@ def get_object(self): return super(EntryViewSet, self).get_object() +class DRFEntryViewSet(viewsets.ModelViewSet): + queryset = Entry.objects.all() + serializer_class = EntryDRFSerializers + lookup_url_kwarg = 'entry_pk' + + def get_object(self): + # Handle featured + entry_pk = self.kwargs.get(self.lookup_url_kwarg, None) + if entry_pk is not None: + return Entry.objects.exclude(pk=entry_pk).first() + + return super(DRFEntryViewSet, self).get_object() + + class NoPagination(PageNumberPagination): page_size = None From 4d7a655d12a7162c70a7b35c8096e522431a9d5a Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 11 Oct 2018 17:21:26 -0400 Subject: [PATCH 18/22] workaround "'PKOnlyObject' object has no attribute" error --- rest_framework_json_api/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 9f3178a1..92e8e01b 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -164,6 +164,12 @@ def get_related_instance(self): field = parent_serializer.fields.get(field_name, None) if field is not None: + # TODO: Workaround, not sure this is a correct fix. + # when many=False (a toOne relationship), must override field.use_pk_only_optimization() + # to return False as `related` needs the attributes + # and raises: `'PKOnlyObject' object has no attribute ''` otherwise. + if hasattr(field, 'use_pk_only_optimization') and field.use_pk_only_optimization(): + field.use_pk_only_optimization = lambda: False return field.get_attribute(parent_obj) else: try: From a3ac0d2e1b47a91ca45219dfb9a5570c82c42747 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 11:09:12 -0400 Subject: [PATCH 19/22] add new Course and Term models to reproduce "'PKOnlyObject' object has no attribute": https://github.com/django-json-api/django-rest-framework-json-api/issues/489 --- example/fixtures/drf_example.json | 422 +++++++++++++++++++++++++ example/migrations/0006_course_term.py | 52 +++ example/models.py | 55 ++++ example/serializers.py | 91 +++++- example/urls.py | 21 +- example/urls_test.py | 21 +- example/views.py | 38 ++- 7 files changed, 695 insertions(+), 5 deletions(-) create mode 100644 example/migrations/0006_course_term.py diff --git a/example/fixtures/drf_example.json b/example/fixtures/drf_example.json index 498c0d1c..3b97f6ed 100644 --- a/example/fixtures/drf_example.json +++ b/example/fixtures/drf_example.json @@ -120,5 +120,427 @@ "body": "Frist comment!!!", "author": null } +}, +{ + "model": "example.course", + "pk": "001b55e0-9a60-4386-98c7-4c856bb840b4", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "ANTB", + "course_number": "04961", + "course_identifier": "ANTH3160V", + "course_name": "THE BODY AND SOCIETY", + "course_description": "THE BODY AND SOCIETY" + } +}, +{ + "model": "example.course", + "pk": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73272", + "course_identifier": "ACCT8122B", + "course_name": "Accounting for Consultants", + "course_description": "Accounting for Consultants" + } +}, +{ + "model": "example.course", + "pk": "016659e9-e29f-49b4-b85d-d25da0724dbb", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73290", + "course_identifier": "ACCT7022B", + "course_name": "Accounting for Value", + "course_description": "Accounting for Value" + } +}, +{ + "model": "example.course", + "pk": "01ca197f-c00c-4f24-a743-091b62f1d500", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "AMSB", + "course_number": "00373", + "course_identifier": "AMST3704X", + "course_name": "SENIOR RESEARCH ESSAY SEMINAR", + "course_description": "SENIOR RESEARCH ESSAY SEMINAR" + } +}, +{ + "model": "example.course", + "pk": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "O", + "suffix_two": "00", + "subject_area_code": "AUDT", + "course_number": "87448", + "course_identifier": "AUPH1010O", + "course_name": "METHODS/PROB OF PHILOS THOUGHT", + "course_description": "METHODS/PROB OF PHILOS THOUGHT" + } +}, +{ + "model": "example.course", + "pk": "02e2e004-326e-4be8-aecc-aa67ece50fdf", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGF2U", + "suffix_two": "00", + "subject_area_code": "COMS", + "course_number": "84695", + "course_identifier": "COMS3102W", + "course_name": "DEVELOPMENT TECHNOLOGY", + "course_description": "MODERN iOS APPLICATION DEVELOP" + } +}, +{ + "model": "example.course", + "pk": "0381673f-e0a4-4212-b95a-3b62ebff9267", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "IG", + "suffix_two": "00", + "subject_area_code": "APAM", + "course_number": "74450", + "course_identifier": "APPH9143E", + "course_name": "APPLIED PHYSICS SEMINAR", + "course_description": "STELLARATOR PHYSICS" + } +}, +{ + "model": "example.course", + "pk": "03e32754-3da7-4005-be6b-8de0e088816a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEF", + "suffix_two": "00", + "subject_area_code": "CEEM", + "course_number": "26118", + "course_identifier": "CIEN3304E", + "course_name": "IND STUDIES-CIVIL ENGIN-SENIOR", + "course_description": "IND STUDIES-CIVIL ENGIN-SENIOR" + } +}, +{ + "model": "example.course", + "pk": "046741cd-c700-4752-b57a-e37a948ebc44", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "BUEC", + "course_number": "72074", + "course_identifier": "BUEC7255B", + "course_name": "FinTech: Consumer Financial Se", + "course_description": "FinTech: Consumer Financial Se" + } +}, +{ + "model": "example.course", + "pk": "04893b8f-0cbe-4e09-b8e6-17a4745900c1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGFKU", + "suffix_two": "00", + "subject_area_code": "ENCL", + "course_number": "89695", + "course_identifier": "CLEN4414W", + "course_name": "HIST OF LITERARY CRITICISM:PLATO TO KANT", + "course_description": "HIST OF LIT CRIT PLATO TO KANT" + } +}, +{ + "model": "example.term", + "pk": "00290ba0-ebae-44c0-9f4b-58a5f27240ed", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "00d14ddb-9fb5-4cff-9954-d52fc33217e7", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "010a7ff7-ef5a-4b36-b3ff-9c34e30b76e8", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "01163a94-fc8f-47fe-bb4a-5407ad1a35fe", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } +}, +{ + "model": "example.term", + "pk": "01764ebb-34b7-4b21-8835-cf712532cf5c", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20182", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "02e877b2-35c4-47d4-b72c-25bab1e87065", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "0316e6ad-fe6a-4339-8d18-a98e4ffb0bee", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "035c31c5-398d-43b7-a55b-19f6d1472797", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "0378c6c0-b658-4cf6-b8ba-6fa19614e3aa", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20192", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "243e2b9c-a3c6-4d40-9b9a-2750d6c03250", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "2d763c14-a566-4600-860f-329e44cbbd4a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "39ca7b38-f273-4fa3-9494-5a422780aebd", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "52cc86dd-7a78-48b8-a6a5-76c1fc7fc9be", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "964ff272-acb8-4adc-9a7e-21a241e63ff1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "bca761f7-03f6-4ff5-bbb8-b58467ef3970", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "e8e13192-0677-44e7-a590-606a38d66b34", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "f9057456-fed6-4982-bb82-3276999cb1ae", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "f9aa1a51-bf3b-45cf-b1cc-34ce47ca9913", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } } ] diff --git a/example/migrations/0006_course_term.py b/example/migrations/0006_course_term.py new file mode 100644 index 00000000..53496898 --- /dev/null +++ b/example/migrations/0006_course_term.py @@ -0,0 +1,52 @@ +# Generated by Django 2.1 on 2018-10-13 09:33 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('example', '0005_auto_20180922_1508'), + ] + + operations = [ + migrations.CreateModel( + name='Course', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('effective_start_date', models.DateField(blank=True, default=None, null=True)), + ('effective_end_date', models.DateField(blank=True, default=None, null=True)), + ('last_mod_user_name', models.CharField(max_length=80)), + ('last_mod_date', models.DateField(auto_now=True)), + ('school_bulletin_prefix_code', models.CharField(max_length=10)), + ('suffix_two', models.CharField(max_length=2)), + ('subject_area_code', models.CharField(max_length=10)), + ('course_number', models.CharField(max_length=10)), + ('course_identifier', models.CharField(max_length=10, unique=True)), + ('course_name', models.CharField(max_length=80)), + ('course_description', models.TextField()), + ], + options={ + 'ordering': ['course_number'], + }, + ), + migrations.CreateModel( + name='Term', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('effective_start_date', models.DateField(blank=True, default=None, null=True)), + ('effective_end_date', models.DateField(blank=True, default=None, null=True)), + ('last_mod_user_name', models.CharField(max_length=80)), + ('last_mod_date', models.DateField(auto_now=True)), + ('term_identifier', models.TextField(max_length=10)), + ('audit_permitted_code', models.PositiveIntegerField(blank=True, default=0)), + ('exam_credit_flag', models.BooleanField(default=True)), + ('course', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='course', to='example.Course')), + ], + options={ + 'ordering': ['term_identifier'], + }, + ), + ] diff --git a/example/models.py b/example/models.py index f183391e..0ac0ef06 100644 --- a/example/models.py +++ b/example/models.py @@ -7,6 +7,8 @@ from django.utils.encoding import python_2_unicode_compatible from polymorphic.models import PolymorphicModel +import uuid + class BaseModel(models.Model): """ @@ -152,3 +154,56 @@ class Company(models.Model): def __str__(self): return self.name + + +# the following serializers are to reproduce/confirm fix for this bug: +# https://github.com/django-json-api/django-rest-framework-json-api/issues/489 +class CommonModel(models.Model): + """ + Abstract model with common fields for all "real" Models: + - id: globally unique UUID version 4 + - effective dates + - last modified dates + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + effective_start_date = models.DateField(default=None, blank=True, null=True) + effective_end_date = models.DateField(default=None, blank=True, null=True) + last_mod_user_name = models.CharField(max_length=80) + last_mod_date = models.DateField(auto_now=True) + + class Meta: + abstract = True + + +class Course(CommonModel): + """ + A course of instruction. e.g. COMSW1002 Computing in Context + """ + school_bulletin_prefix_code = models.CharField(max_length=10) + suffix_two = models.CharField(max_length=2) + subject_area_code = models.CharField(max_length=10) + course_number = models.CharField(max_length=10) + course_identifier = models.CharField(max_length=10, unique=True) + course_name = models.CharField(max_length=80) + course_description = models.TextField() + + class Meta: + # verbose_name = "Course" + # verbose_name_plural = "Courses" + ordering = ["course_number"] + + +class Term(CommonModel): + """ + A specific course term (year+semester) instance. + e.g. 20183COMSW1002 + """ + term_identifier = models.TextField(max_length=10) + audit_permitted_code = models.PositiveIntegerField(blank=True, default=0) + exam_credit_flag = models.BooleanField(default=True) + course = models.ForeignKey('example.Course', related_name='terms', + on_delete=models.CASCADE, null=True, + default=None) + + class Meta: + ordering = ["term_identifier"] diff --git a/example/serializers.py b/example/serializers.py index c52f575f..23f17b99 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -16,7 +16,9 @@ Project, ProjectType, ResearchProject, - TaggedItem + TaggedItem, + Course, + Term, ) @@ -313,3 +315,90 @@ class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company fields = '__all__' + + +# the following serializers are to reproduce/confirm fix for this bug: +# https://github.com/django-json-api/django-rest-framework-json-api/issues/489 +class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): + """ + .models.CommonModel.last_mod_user_name/date should come from auth.user on a POST/PATCH + """ + def _last_mod(self, validated_data): + """ + override any last_mod_user_name or date with current auth user and current date. + """ + validated_data['last_mod_user_name'] = self.context['request'].user + validated_data['last_mod_date'] = datetime.now().date() + + def create(self, validated_data): + """ + extended ModelSerializer to set last_mod_user/date + """ + self._last_mod(validated_data) + return super(HyperlinkedModelSerializer, self).create(validated_data) + + def update(self, validated_data): + """ + extended ModelSerializer to set last_mod_user/date + """ + self._last_mod(validated_data) + return super(HyperlinkedModelSerializer, self).update(validated_data) + + +class CourseSerializer(HyperlinkedModelSerializer): + """ + (de-)serialize the Course. + """ + terms = relations.ResourceRelatedField( + model=Term, + many=True, + read_only=False, + allow_null=True, + required=False, + queryset=Term.objects.all(), + self_link_view_name='course-relationships', + related_link_view_name='course-related', + ) + + # 'included' support (also used for `related_serializers` for DJA 2.6.0) + included_serializers = { + 'terms': 'example.serializers.TermSerializer', + } + + class Meta: + model = Course + fields = ( + 'url', + 'school_bulletin_prefix_code', 'suffix_two', 'subject_area_code', + 'course_number', 'course_identifier', 'course_name', 'course_description', + 'effective_start_date', 'effective_end_date', + 'last_mod_user_name', 'last_mod_date', + 'terms') + + +class TermSerializer(HyperlinkedModelSerializer): + course = relations.ResourceRelatedField( + model=Course, + many=False, # this breaks new 2.6.0 related support. Only works when True. + read_only=False, + allow_null=True, + required=False, + queryset=Course.objects.all(), + self_link_view_name='term-relationships', + related_link_view_name='term-related', + ) + + included_serializers = { + 'course': 'example.serializers.CourseSerializer', + } + + class Meta: + model = Term + fields = ( + 'url', + 'term_identifier', 'audit_permitted_code', + 'exam_credit_flag', + 'effective_start_date', 'effective_end_date', + 'last_mod_user_name', 'last_mod_date', + 'course') + diff --git a/example/urls.py b/example/urls.py index 79d3b1c1..d7eeef4c 100644 --- a/example/urls.py +++ b/example/urls.py @@ -14,7 +14,11 @@ EntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + CourseViewSet, + TermViewSet, + CourseRelationshipView, + TermRelationshipView, ) router = routers.DefaultRouter(trailing_slash=False) @@ -27,6 +31,8 @@ router.register(r'companies', CompanyViewset) router.register(r'projects', ProjectViewset) router.register(r'project-types', ProjectTypeViewset) +router.register(r'courses', CourseViewSet) +router.register(r'terms', TermViewSet) urlpatterns = [ url(r'^', include(router.urls)), @@ -63,6 +69,19 @@ url(r'^authors/(?P[^/.]+)/relationships/(?P\w+)', AuthorRelationshipView.as_view(), name='author-relationships'), + + url(r'courses/(?P[^/.]+)/relationships/(?P\w+)', + CourseRelationshipView.as_view(), + name='course-relationships'), + url(r'courses/(?P[^/.]+)/(?P\w+)/$', + CourseViewSet.as_view({'get': 'retrieve_related'}), + name='course-related'), + url(r'terms/(?P[^/.]+)/relationships/(?P\w+)', + TermRelationshipView.as_view(), + name='term-relationships'), + url(r'terms/(?P[^/.]+)/(?P\w+)/$', + TermViewSet.as_view({'get': 'retrieve_related'}), + name='term-related'), ] diff --git a/example/urls_test.py b/example/urls_test.py index e51121ac..ebc4479f 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -18,7 +18,11 @@ NoFiltersetEntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + CourseViewSet, + TermViewSet, + CourseRelationshipView, + TermRelationshipView, ) router = routers.DefaultRouter(trailing_slash=False) @@ -36,6 +40,8 @@ router.register(r'companies', CompanyViewset) router.register(r'projects', ProjectViewset) router.register(r'project-types', ProjectTypeViewset) +router.register(r'courses', CourseViewSet) +router.register(r'terms', TermViewSet) # for the old tests router.register(r'identities', Identity) @@ -87,4 +93,17 @@ url(r'^authors/(?P[^/.]+)/relationships/(?P\w+)', AuthorRelationshipView.as_view(), name='author-relationships'), + + url(r'courses/(?P[^/.]+)/relationships/(?P\w+)', + CourseRelationshipView.as_view(), + name='course-relationships'), + url(r'courses/(?P[^/.]+)/(?P\w+)/$', + CourseViewSet.as_view({'get': 'retrieve_related'}), + name='course-related'), + url(r'terms/(?P[^/.]+)/relationships/(?P\w+)', + TermRelationshipView.as_view(), + name='term-relationships'), + url(r'terms/(?P[^/.]+)/(?P\w+)/$', + TermViewSet.as_view({'get': 'retrieve_related'}), + name='term-related'), ] diff --git a/example/views.py b/example/views.py index 41036fc6..b0232515 100644 --- a/example/views.py +++ b/example/views.py @@ -14,7 +14,17 @@ from rest_framework_json_api.utils import format_drf_errors from rest_framework_json_api.views import ModelViewSet, RelationshipView -from example.models import Author, Blog, Comment, Company, Entry, Project, ProjectType +from example.models import ( + Author, + Blog, + Comment, + Company, + Entry, + Project, + ProjectType, + Course, + Term, +) from example.serializers import ( AuthorSerializer, BlogDRFSerializer, @@ -24,7 +34,9 @@ EntryDRFSerializers, EntrySerializer, ProjectSerializer, - ProjectTypeSerializer + ProjectTypeSerializer, + CourseSerializer, + TermSerializer, ) HTTP_422_UNPROCESSABLE_ENTITY = 422 @@ -227,3 +239,25 @@ class CommentRelationshipView(RelationshipView): class AuthorRelationshipView(RelationshipView): queryset = Author.objects.all() self_link_view_name = 'author-relationships' + + +# the following views are to reproduce/confirm fix for this bug: +# https://github.com/django-json-api/django-rest-framework-json-api/issues/489 +class CourseViewSet(ModelViewSet): + queryset = Course.objects.all() + serializer_class = CourseSerializer + + +class TermViewSet(ModelViewSet): + queryset = Term.objects.all() + serializer_class = TermSerializer + + +class CourseRelationshipView(RelationshipView): + queryset = Course.objects + self_link_view_name = 'course-relationships' + + +class TermRelationshipView(RelationshipView): + queryset = Term.objects + self_link_view_name = 'term-relationships' From 509edfa04e4eb753f9c2fa893af8b657168ca40e Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 12:32:27 -0400 Subject: [PATCH 20/22] use RelatedMixin.override_pk_only_optimization=True to prevent AttributeError exception. --- example/fixtures/courseterm.json | 424 +++++++++++++++++++++++++++++++ example/tests/test_views.py | 28 +- rest_framework_json_api/views.py | 8 +- 3 files changed, 457 insertions(+), 3 deletions(-) create mode 100644 example/fixtures/courseterm.json diff --git a/example/fixtures/courseterm.json b/example/fixtures/courseterm.json new file mode 100644 index 00000000..4ec37109 --- /dev/null +++ b/example/fixtures/courseterm.json @@ -0,0 +1,424 @@ +[ +{ + "model": "example.course", + "pk": "001b55e0-9a60-4386-98c7-4c856bb840b4", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "ANTB", + "course_number": "04961", + "course_identifier": "ANTH3160V", + "course_name": "THE BODY AND SOCIETY", + "course_description": "THE BODY AND SOCIETY" + } +}, +{ + "model": "example.course", + "pk": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73272", + "course_identifier": "ACCT8122B", + "course_name": "Accounting for Consultants", + "course_description": "Accounting for Consultants" + } +}, +{ + "model": "example.course", + "pk": "016659e9-e29f-49b4-b85d-d25da0724dbb", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "ACCT", + "course_number": "73290", + "course_identifier": "ACCT7022B", + "course_name": "Accounting for Value", + "course_description": "Accounting for Value" + } +}, +{ + "model": "example.course", + "pk": "01ca197f-c00c-4f24-a743-091b62f1d500", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEFK9", + "suffix_two": "00", + "subject_area_code": "AMSB", + "course_number": "00373", + "course_identifier": "AMST3704X", + "course_name": "SENIOR RESEARCH ESSAY SEMINAR", + "course_description": "SENIOR RESEARCH ESSAY SEMINAR" + } +}, +{ + "model": "example.course", + "pk": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "O", + "suffix_two": "00", + "subject_area_code": "AUDT", + "course_number": "87448", + "course_identifier": "AUPH1010O", + "course_name": "METHODS/PROB OF PHILOS THOUGHT", + "course_description": "METHODS/PROB OF PHILOS THOUGHT" + } +}, +{ + "model": "example.course", + "pk": "02e2e004-326e-4be8-aecc-aa67ece50fdf", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGF2U", + "suffix_two": "00", + "subject_area_code": "COMS", + "course_number": "84695", + "course_identifier": "COMS3102W", + "course_name": "DEVELOPMENT TECHNOLOGY", + "course_description": "MODERN iOS APPLICATION DEVELOP" + } +}, +{ + "model": "example.course", + "pk": "0381673f-e0a4-4212-b95a-3b62ebff9267", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "IG", + "suffix_two": "00", + "subject_area_code": "APAM", + "course_number": "74450", + "course_identifier": "APPH9143E", + "course_name": "APPLIED PHYSICS SEMINAR", + "course_description": "STELLARATOR PHYSICS" + } +}, +{ + "model": "example.course", + "pk": "03e32754-3da7-4005-be6b-8de0e088816a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "XCEF", + "suffix_two": "00", + "subject_area_code": "CEEM", + "course_number": "26118", + "course_identifier": "CIEN3304E", + "course_name": "IND STUDIES-CIVIL ENGIN-SENIOR", + "course_description": "IND STUDIES-CIVIL ENGIN-SENIOR" + } +}, +{ + "model": "example.course", + "pk": "046741cd-c700-4752-b57a-e37a948ebc44", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "B", + "suffix_two": "00", + "subject_area_code": "BUEC", + "course_number": "72074", + "course_identifier": "BUEC7255B", + "course_name": "FinTech: Consumer Financial Se", + "course_description": "FinTech: Consumer Financial Se" + } +}, +{ + "model": "example.course", + "pk": "04893b8f-0cbe-4e09-b8e6-17a4745900c1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "school_bulletin_prefix_code": "RXCEIGFKU", + "suffix_two": "00", + "subject_area_code": "ENCL", + "course_number": "89695", + "course_identifier": "CLEN4414W", + "course_name": "HIST OF LITERARY CRITICISM:PLATO TO KANT", + "course_description": "HIST OF LIT CRIT PLATO TO KANT" + } +}, +{ + "model": "example.term", + "pk": "00290ba0-ebae-44c0-9f4b-58a5f27240ed", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "00d14ddb-9fb5-4cff-9954-d52fc33217e7", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "010a7ff7-ef5a-4b36-b3ff-9c34e30b76e8", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "01163a94-fc8f-47fe-bb4a-5407ad1a35fe", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } +}, +{ + "model": "example.term", + "pk": "01764ebb-34b7-4b21-8835-cf712532cf5c", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20182", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "02e877b2-35c4-47d4-b72c-25bab1e87065", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "0316e6ad-fe6a-4339-8d18-a98e4ffb0bee", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20191", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "035c31c5-398d-43b7-a55b-19f6d1472797", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20183", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "0378c6c0-b658-4cf6-b8ba-6fa19614e3aa", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20192", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "243e2b9c-a3c6-4d40-9b9a-2750d6c03250", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "001b55e0-9a60-4386-98c7-4c856bb840b4" + } +}, +{ + "model": "example.term", + "pk": "2d763c14-a566-4600-860f-329e44cbbd4a", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02e2e004-326e-4be8-aecc-aa67ece50fdf" + } +}, +{ + "model": "example.term", + "pk": "39ca7b38-f273-4fa3-9494-5a422780aebd", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "016659e9-e29f-49b4-b85d-d25da0724dbb" + } +}, +{ + "model": "example.term", + "pk": "52cc86dd-7a78-48b8-a6a5-76c1fc7fc9be", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "00fb17bb-e4a0-49a0-a27e-6939e3e04b62" + } +}, +{ + "model": "example.term", + "pk": "964ff272-acb8-4adc-9a7e-21a241e63ff1", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "03e32754-3da7-4005-be6b-8de0e088816a" + } +}, +{ + "model": "example.term", + "pk": "bca761f7-03f6-4ff5-bbb8-b58467ef3970", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "046741cd-c700-4752-b57a-e37a948ebc44" + } +}, +{ + "model": "example.term", + "pk": "e8e13192-0677-44e7-a590-606a38d66b34", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "02d5fcbc-cc6d-40e0-9acf-5f4e54cf3d87" + } +}, +{ + "model": "example.term", + "pk": "f9057456-fed6-4982-bb82-3276999cb1ae", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "0381673f-e0a4-4212-b95a-3b62ebff9267" + } +}, +{ + "model": "example.term", + "pk": "f9aa1a51-bf3b-45cf-b1cc-34ce47ca9913", + "fields": { + "effective_start_date": null, + "effective_end_date": null, + "last_mod_user_name": "loader", + "last_mod_date": "2018-08-03", + "term_identifier": "20181", + "audit_permitted_code": 0, + "exam_credit_flag": false, + "course": "01ca197f-c00c-4f24-a743-091b62f1d500" + } +} +] diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 5b778b90..cbe0f8ce 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,4 +1,5 @@ import json +import mock from django.test import RequestFactory from django.utils import timezone @@ -12,7 +13,7 @@ from . import TestBase from .. import views from example.factories import AuthorFactory, CommentFactory, EntryFactory -from example.models import Author, Blog, Comment, Entry +from example.models import Author, Blog, Comment, Entry, Course, Term from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer from example.views import AuthorViewSet @@ -275,9 +276,12 @@ def test_new_comment_data_patch_to_many_relationship(self): class TestRelatedMixin(APITestCase): + fixtures = ('courseterm',) def setUp(self): self.author = AuthorFactory() + self.course = Course.objects.all() + self.term = Term.objects.all() def _get_view(self, kwargs): factory = APIRequestFactory() @@ -363,6 +367,28 @@ def test_retrieve_related_None(self): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json(), {'data': None}) + # the following tests are to reproduce/confirm fix for this bug: + # https://github.com/django-json-api/django-rest-framework-json-api/issues/489 + def test_term_related_course(self): + """ + confirm that the related data reference the primary key + """ + term_id = self.term.first().pk + kwargs = {'pk': term_id, 'related_field': 'course'} + url = reverse('term-related', kwargs=kwargs) + with mock.patch('rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + True): + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + dja_response = resp.json() + back_reference = dja_response['data']['relationships']['terms']['data'] + self.assertIn({"type": "terms", "id": str(term_id)}, back_reference) + + # the following raises AttributeError: + with mock.patch('rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + False): + resp = self.client.get(url) + class TestValidationErrorResponses(TestBase): def test_if_returns_error_on_empty_post(self): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 92e8e01b..93ff5ad1 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -107,6 +107,9 @@ class RelatedMixin(object): """ This mixin handles all related entities, whose Serializers are declared in "related_serializers" """ + # test bug fix for https://github.com/django-json-api/django-rest-framework-json-api/issues/489 + #: override pk_only optimization + override_pk_only_optimization = False def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} @@ -168,8 +171,9 @@ def get_related_instance(self): # when many=False (a toOne relationship), must override field.use_pk_only_optimization() # to return False as `related` needs the attributes # and raises: `'PKOnlyObject' object has no attribute ''` otherwise. - if hasattr(field, 'use_pk_only_optimization') and field.use_pk_only_optimization(): - field.use_pk_only_optimization = lambda: False + if self.override_pk_only_optimization: + if hasattr(field, 'use_pk_only_optimization') and field.use_pk_only_optimization(): + field.use_pk_only_optimization = lambda: False return field.get_attribute(parent_obj) else: try: From 875b56bc5182416f4e8fe7aef83490c21c1833c5 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 13:02:10 -0400 Subject: [PATCH 21/22] default override_pk_only_optimization True --- rest_framework_json_api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 93ff5ad1..3e01f496 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -109,7 +109,7 @@ class RelatedMixin(object): """ # test bug fix for https://github.com/django-json-api/django-rest-framework-json-api/issues/489 #: override pk_only optimization - override_pk_only_optimization = False + override_pk_only_optimization = True def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} From 2262aa67833c1c913888d161e1b154475d9033a4 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 13 Oct 2018 13:03:59 -0400 Subject: [PATCH 22/22] tox, flake8, isort, make test exception a passing test --- example/models.py | 4 ++-- example/serializers.py | 4 ++-- example/tests/test_views.py | 20 ++++++++++++++------ example/urls.py | 6 +++--- example/urls_test.py | 6 +++--- example/views.py | 16 +++------------- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/example/models.py b/example/models.py index 0ac0ef06..9dea768e 100644 --- a/example/models.py +++ b/example/models.py @@ -1,14 +1,14 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals +import uuid + from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.encoding import python_2_unicode_compatible from polymorphic.models import PolymorphicModel -import uuid - class BaseModel(models.Model): """ diff --git a/example/serializers.py b/example/serializers.py index 23f17b99..d148952d 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -12,13 +12,13 @@ Blog, Comment, Company, + Course, Entry, Project, ProjectType, ResearchProject, TaggedItem, - Course, - Term, + Term ) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index cbe0f8ce..adfdee20 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,5 +1,4 @@ import json -import mock from django.test import RequestFactory from django.utils import timezone @@ -17,6 +16,11 @@ from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer from example.views import AuthorViewSet +try: + from unittest import mock +except ImportError: + import mock + class TestRelationshipView(APITestCase): def setUp(self): @@ -367,11 +371,11 @@ def test_retrieve_related_None(self): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json(), {'data': None}) - # the following tests are to reproduce/confirm fix for this bug: + # the following test reproduces/confirm fix for this bug: # https://github.com/django-json-api/django-rest-framework-json-api/issues/489 def test_term_related_course(self): """ - confirm that the related data reference the primary key + confirm that the related child data reference the parent """ term_id = self.term.first().pk kwargs = {'pk': term_id, 'related_field': 'course'} @@ -385,9 +389,13 @@ def test_term_related_course(self): self.assertIn({"type": "terms", "id": str(term_id)}, back_reference) # the following raises AttributeError: - with mock.patch('rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', - False): - resp = self.client.get(url) + with self.assertRaises(AttributeError) as ae: + with mock.patch( + 'rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + False): + resp = self.client.get(url) + print(ae.exception) + self.assertIn('`PKOnlyObject`', ae.exception.args[0]) class TestValidationErrorResponses(TestBase): diff --git a/example/urls.py b/example/urls.py index d7eeef4c..65162b73 100644 --- a/example/urls.py +++ b/example/urls.py @@ -10,15 +10,15 @@ CommentRelationshipView, CommentViewSet, CompanyViewset, + CourseRelationshipView, + CourseViewSet, EntryRelationshipView, EntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, ProjectViewset, - CourseViewSet, - TermViewSet, - CourseRelationshipView, TermRelationshipView, + TermViewSet ) router = routers.DefaultRouter(trailing_slash=False) diff --git a/example/urls_test.py b/example/urls_test.py index ebc4479f..421fbc6f 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -12,6 +12,8 @@ CompanyViewset, DRFBlogViewSet, DRFEntryViewSet, + CourseRelationshipView, + CourseViewSet, EntryRelationshipView, EntryViewSet, FiltersetEntryViewSet, @@ -19,10 +21,8 @@ NonPaginatedEntryViewSet, ProjectTypeViewset, ProjectViewset, - CourseViewSet, - TermViewSet, - CourseRelationshipView, TermRelationshipView, + TermViewSet ) router = routers.DefaultRouter(trailing_slash=False) diff --git a/example/views.py b/example/views.py index b0232515..a56c160e 100644 --- a/example/views.py +++ b/example/views.py @@ -14,17 +14,7 @@ from rest_framework_json_api.utils import format_drf_errors from rest_framework_json_api.views import ModelViewSet, RelationshipView -from example.models import ( - Author, - Blog, - Comment, - Company, - Entry, - Project, - ProjectType, - Course, - Term, -) +from example.models import Author, Blog, Comment, Company, Course, Entry, Project, ProjectType, Term from example.serializers import ( AuthorSerializer, BlogDRFSerializer, @@ -32,11 +22,11 @@ CommentSerializer, CompanySerializer, EntryDRFSerializers, + CourseSerializer, EntrySerializer, ProjectSerializer, ProjectTypeSerializer, - CourseSerializer, - TermSerializer, + TermSerializer ) HTTP_422_UNPROCESSABLE_ENTITY = 422