diff --git a/CHANGELOG.md b/CHANGELOG.md index c60329ef..b278cb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ any parts of the framework not mentioned in the documentation should generally b * 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)) - +* Fixes `retrieve_related` PKOnlyObject attribute error for a to-one relationship ([#489](https://github.com/django-json-api/django-rest-framework-json-api/issues/489)) ## [2.6.0] - 2018-09-20 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/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..9dea768e 100644 --- a/example/models.py +++ b/example/models.py @@ -1,6 +1,8 @@ # -*- 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 @@ -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..5749f001 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -12,11 +12,13 @@ Blog, Comment, Company, + Course, Entry, Project, ProjectType, ResearchProject, - TaggedItem + TaggedItem, + Term ) @@ -313,3 +315,89 @@ 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/tests/test_views.py b/example/tests/test_views.py index 5b778b90..97b48c27 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -12,10 +12,15 @@ 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, 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): @@ -275,9 +280,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 +371,31 @@ def test_retrieve_related_None(self): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json(), {'data': None}) + # the following test reproduces/confirms the 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 child data references the parent + """ + 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 self.assertRaises(AttributeError) as ae: + with mock.patch( + 'rest_framework_json_api.views.RelatedMixin.override_pk_only_optimization', + False): + resp = self.client.get(url) + self.assertIn('`PKOnlyObject`', ae.exception.args[0]) + class TestValidationErrorResponses(TestBase): def test_if_returns_error_on_empty_post(self): diff --git a/example/urls.py b/example/urls.py index 79d3b1c1..65162b73 100644 --- a/example/urls.py +++ b/example/urls.py @@ -10,11 +10,15 @@ CommentRelationshipView, CommentViewSet, CompanyViewset, + CourseRelationshipView, + CourseViewSet, EntryRelationshipView, EntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + TermRelationshipView, + TermViewSet ) 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..54825dbb 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -10,6 +10,8 @@ CommentRelationshipView, CommentViewSet, CompanyViewset, + CourseRelationshipView, + CourseViewSet, DRFBlogViewSet, DRFEntryViewSet, EntryRelationshipView, @@ -18,7 +20,9 @@ NoFiltersetEntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + TermRelationshipView, + TermViewSet ) 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..832dc36d 100644 --- a/example/views.py +++ b/example/views.py @@ -14,17 +14,19 @@ 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, Course, Entry, Project, ProjectType, Term from example.serializers import ( AuthorSerializer, BlogDRFSerializer, BlogSerializer, CommentSerializer, CompanySerializer, + CourseSerializer, EntryDRFSerializers, EntrySerializer, ProjectSerializer, - ProjectTypeSerializer + ProjectTypeSerializer, + TermSerializer ) HTTP_422_UNPROCESSABLE_ENTITY = 422 @@ -227,3 +229,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' diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 9f3178a1..3e01f496 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 = True def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} @@ -164,6 +167,13 @@ 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 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: