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 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..c60329ef 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 @@ -20,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/README.rst b/README.rst index 13579a24..61ea29c2 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 + $ pytest ----- 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/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 1fee79c4..d148952d 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -12,11 +12,13 @@ Blog, Comment, Company, + Course, Entry, Project, ProjectType, ResearchProject, - TaggedItem + TaggedItem, + Term ) @@ -63,7 +65,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 +175,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 @@ -298,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/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/example/tests/test_views.py b/example/tests/test_views.py index 48e1bfa6..adfdee20 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -11,11 +11,16 @@ from . import TestBase from .. import views -from example.factories import AuthorFactory, EntryFactory -from example.models import Author, Blog, Comment, Entry +from example.factories import AuthorFactory, CommentFactory, EntryFactory +from example.models import Author, Blog, Comment, Entry, Course, 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): @@ -229,11 +234,58 @@ 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): + 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 +371,32 @@ def test_retrieve_related_None(self): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json(), {'data': None}) + # 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 child data reference 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) + print(ae.exception) + self.assertIn('`PKOnlyObject`', ae.exception.args[0]) + class TestValidationErrorResponses(TestBase): def test_if_returns_error_on_empty_post(self): 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.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 2e7d2d64..4cb43fc0 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -10,20 +10,27 @@ CommentRelationshipView, CommentViewSet, CompanyViewset, + CourseRelationshipView, + CourseViewSet, DRFBlogViewSet, + DRFEntryViewSet, + CourseRelationshipView, + CourseViewSet, EntryRelationshipView, EntryViewSet, FiltersetEntryViewSet, NoFiltersetEntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset + ProjectViewset, + TermRelationshipView, + TermViewSet ) 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: @@ -35,6 +42,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) @@ -59,6 +68,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'), @@ -82,4 +95,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 78cdc6ad..a56c160e 100644 --- a/example/views.py +++ b/example/views.py @@ -14,16 +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, + EntryDRFSerializers, + CourseSerializer, EntrySerializer, ProjectSerializer, - ProjectTypeSerializer + ProjectTypeSerializer, + TermSerializer ) HTTP_422_UNPROCESSABLE_ENTITY = 422 @@ -104,6 +107,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 @@ -212,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/requirements-development.txt b/requirements-development.txt index 834dc094..9a96f4f1 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 -flake8 -flake8-isort -isort -mock -pytest -pytest-django -pytest-factoryboy -recommonmark -Sphinx -sphinx_rtd_theme -twine +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==2.5 +isort==4.3.4 +mock==2.0.0 +pytest==3.10.1 +pytest-django==3.4.3 +pytest-factoryboy==2.0.2 +recommonmark==0.4.0 +Sphinx==1.8.2 +sphinx_rtd_theme==0.4.2 +twine==1.12.1 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/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 ) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index f8766bc4..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: @@ -251,6 +261,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 +283,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': 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= 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