diff --git a/example_projects/jsonplaceholder_project/todos/rest_handler.py b/example_projects/jsonplaceholder_project/todos/rest_handler.py
index fc58ef1..bfc6db0 100644
--- a/example_projects/jsonplaceholder_project/todos/rest_handler.py
+++ b/example_projects/jsonplaceholder_project/todos/rest_handler.py
@@ -58,7 +58,7 @@ def update(self, *, model, pk, values) -> Optional[int]:
Return num rows changed
"""
data = {}
- for col, model, value in values:
+ for col, _, value in values:
data[self.COLUMN_MAPPING.get(col.name, col.name)] = value
r = requests.put(f"https://jsonplaceholder.typicode.com/todos/{pk}", json=data)
diff --git a/example_projects/jsonserver-project/README.md b/example_projects/jsonserver-project/README.md
new file mode 100644
index 0000000..a066a75
--- /dev/null
+++ b/example_projects/jsonserver-project/README.md
@@ -0,0 +1,44 @@
+
+# Example project: typicode/json-server
+
+
+This example project uses the full fake REST API from [typicode/json-server](https://github.com/typicode/json-server)
+
+Please note that this API is readonly and changes (insert, update & delete) won't be applied!
+
+## Running example project
+
+
+1. Create and activate Virtual Environment
+
+ python3 -m venv .venv
+ source .venv/bin/activate
+
+2. Install requirements:
+
+ pip install -r requirements.txt
+
+3. Create local SQLite database:
+
+ python manage.py migrate
+
+4. Load sample data:
+
+ python manage.py loaddata mysite/fixtures/initial_data
+
+5. Run Django server:
+
+ python manage.py runserver
+
+6. Install JSON server:
+
+ npm install -g json-server
+
+6. Run JSON server:
+
+ json-server --watch db.json
+
+
+6. Take a look at:
+ - Movies view:
+ - Django Admin: (log in with username `admin` and password `admin`)
diff --git a/example_projects/jsonserver-project/db.json b/example_projects/jsonserver-project/db.json
new file mode 100644
index 0000000..e4f7dba
--- /dev/null
+++ b/example_projects/jsonserver-project/db.json
@@ -0,0 +1,479 @@
+{
+ "movies": [
+ {
+ "id": 1,
+ "title": "White Banners",
+ "description": "sapien quis libero nullam sit amet turpis elementum ligula vehicula consequat morbi a ipsum integer a nibh in quis",
+ "genre": "Drama",
+ "rating": 4,
+ "country_code": "ID",
+ "release_date": "1975-06-04",
+ "is_most_watched": false,
+ "created_at": "2022-12-16T01:49:05Z"
+ },
+ {
+ "id": 2,
+ "title": "Wizard of Baghdad, The",
+ "description": "nunc viverra dapibus nulla suscipit ligula in lacus curabitur at ipsum ac tellus semper interdum mauris ullamcorper purus sit amet nulla quisque arcu libero rutrum",
+ "genre": "Comedy|Fantasy|Musical",
+ "rating": 7,
+ "country_code": "RU",
+ "release_date": "1984-06-17",
+ "is_most_watched": false,
+ "created_at": "2022-09-10T14:20:37Z"
+ },
+ {
+ "id": 3,
+ "title": "Wankers, The (Les branleuses)",
+ "description": "dictumst maecenas ut massa quis augue luctus tincidunt nulla mollis molestie lorem quisque ut erat curabitur gravida nisi at nibh in hac habitasse platea dictumst aliquam augue quam sollicitudin",
+ "genre": "Documentary",
+ "rating": 6,
+ "country_code": "CN",
+ "release_date": "2005-12-03",
+ "is_most_watched": false,
+ "created_at": "2022-08-25T10:51:16Z"
+ },
+ {
+ "id": 4,
+ "title": "Christopher Columbus: The Discovery",
+ "description": "id pretium iaculis diam erat fermentum justo nec condimentum neque sapien placerat ante nulla justo aliquam quis turpis",
+ "genre": "Adventure",
+ "rating": 1,
+ "country_code": "CN",
+ "release_date": "1978-08-12",
+ "is_most_watched": true,
+ "created_at": "2022-11-14T01:15:28Z"
+ },
+ {
+ "id": 5,
+ "title": "Where the Day Takes You",
+ "description": "condimentum id luctus nec molestie sed justo pellentesque viverra pede ac diam cras pellentesque volutpat dui maecenas tristique est et",
+ "genre": "Drama",
+ "rating": 3,
+ "country_code": "PH",
+ "release_date": "1953-08-29",
+ "is_most_watched": true,
+ "created_at": "2022-08-07T18:22:41Z"
+ },
+ {
+ "id": 6,
+ "title": "Visitor Q (Bizita Q)",
+ "description": "aliquam erat volutpat in congue etiam justo etiam pretium iaculis justo in hac habitasse platea dictumst etiam faucibus cursus urna ut tellus nulla ut erat id mauris",
+ "genre": "Comedy|Drama|Horror",
+ "rating": 10,
+ "country_code": "RU",
+ "release_date": "2015-11-08",
+ "is_most_watched": true,
+ "created_at": "2022-09-05T21:55:52Z"
+ },
+ {
+ "id": 7,
+ "title": "Judith of Bethulia",
+ "description": "eleifend luctus ultricies eu nibh quisque id justo sit amet sapien dignissim vestibulum vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae nulla dapibus dolor",
+ "genre": "Adventure|Drama|Romance",
+ "rating": 4,
+ "country_code": "UA",
+ "release_date": "1955-06-23",
+ "is_most_watched": false,
+ "created_at": "2022-11-16T03:39:13Z"
+ },
+ {
+ "id": 8,
+ "title": "Slaughter Rule, The",
+ "description": "ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae nulla dapibus dolor vel est donec odio justo sollicitudin ut suscipit a feugiat",
+ "genre": "Drama",
+ "rating": 3,
+ "country_code": "FR",
+ "release_date": "2002-09-03",
+ "is_most_watched": false,
+ "created_at": "2022-05-22T01:00:49Z"
+ },
+ {
+ "id": 9,
+ "title": "My Wrongs 8245-8249 and 117",
+ "description": "nascetur ridiculus mus etiam vel augue vestibulum rutrum rutrum neque aenean auctor gravida sem praesent id massa id",
+ "genre": "Comedy|Drama",
+ "rating": 10,
+ "country_code": "CN",
+ "release_date": "1974-11-11",
+ "is_most_watched": false,
+ "created_at": "2022-11-18T16:38:48Z"
+ },
+ {
+ "id": 10,
+ "title": "Wicker Man, The",
+ "description": "in quam fringilla rhoncus mauris enim leo rhoncus sed vestibulum sit amet cursus id turpis integer aliquet",
+ "genre": "Horror|Mystery|Thriller",
+ "rating": 1,
+ "country_code": "LA",
+ "release_date": "2003-02-04",
+ "is_most_watched": true,
+ "created_at": "2022-08-22T06:05:14Z"
+ },
+ {
+ "id": 11,
+ "title": "Lilian's Story",
+ "description": "turpis enim blandit mi in porttitor pede justo eu massa donec dapibus duis at velit eu est congue elementum in hac habitasse platea dictumst morbi vestibulum velit",
+ "genre": "Drama",
+ "rating": 6,
+ "country_code": "FR",
+ "release_date": "1954-11-21",
+ "is_most_watched": true,
+ "created_at": "2022-04-15T01:11:19Z"
+ },
+ {
+ "id": 12,
+ "title": "What Maisie Knew",
+ "description": "porttitor id consequat in consequat ut nulla sed accumsan felis ut at dolor quis odio consequat varius integer ac leo pellentesque ultrices mattis odio donec vitae nisi nam",
+ "genre": "Drama",
+ "rating": 10,
+ "country_code": "PH",
+ "release_date": "2013-02-18",
+ "is_most_watched": true,
+ "created_at": "2022-02-09T03:21:18Z"
+ },
+ {
+ "id": 13,
+ "title": "112 Weddings",
+ "description": "non sodales sed tincidunt eu felis fusce posuere felis sed lacus morbi sem mauris laoreet ut rhoncus aliquet",
+ "genre": "Documentary|Romance",
+ "rating": 9,
+ "country_code": "PT",
+ "release_date": "2016-08-25",
+ "is_most_watched": false,
+ "created_at": "2022-03-09T03:30:56Z"
+ },
+ {
+ "id": 14,
+ "title": "Doppelgänger Paul",
+ "description": "suspendisse potenti in eleifend quam a odio in hac habitasse platea dictumst maecenas ut massa quis augue luctus tincidunt nulla mollis molestie lorem quisque ut erat curabitur",
+ "genre": "(no genres listed)",
+ "rating": 8,
+ "country_code": "RU",
+ "release_date": "2000-05-14",
+ "is_most_watched": false,
+ "created_at": "2022-11-28T07:10:16Z"
+ },
+ {
+ "id": 15,
+ "title": "How High",
+ "description": "odio cras mi pede malesuada in imperdiet et commodo vulputate justo in blandit ultrices enim lorem ipsum dolor sit amet consectetuer adipiscing elit proin interdum mauris non ligula pellentesque ultrices",
+ "genre": "Comedy",
+ "rating": 3,
+ "country_code": "CF",
+ "release_date": "1967-10-31",
+ "is_most_watched": true,
+ "created_at": "2022-10-08T13:55:50Z"
+ },
+ {
+ "id": 16,
+ "title": "Puss in Boots: The Three Diablos",
+ "description": "nisi volutpat eleifend donec ut dolor morbi vel lectus in quam fringilla rhoncus mauris enim leo rhoncus sed vestibulum",
+ "genre": "Animation|Comedy",
+ "rating": 10,
+ "country_code": "PS",
+ "release_date": "1972-11-10",
+ "is_most_watched": true,
+ "created_at": "2023-01-11T23:51:18Z"
+ },
+ {
+ "id": 17,
+ "title": "Rendition",
+ "description": "nisl nunc nisl duis bibendum felis sed interdum venenatis turpis enim blandit mi in porttitor pede justo eu massa donec dapibus duis at",
+ "genre": "Drama|Thriller",
+ "rating": 9,
+ "country_code": "AL",
+ "release_date": "2016-07-27",
+ "is_most_watched": true,
+ "created_at": "2022-01-25T21:41:38Z"
+ },
+ {
+ "id": 18,
+ "title": "Reclaim",
+ "description": "cubilia curae mauris viverra diam vitae quam suspendisse potenti nullam porttitor lacus at turpis donec posuere metus vitae ipsum aliquam non mauris morbi non lectus",
+ "genre": "Drama|Thriller",
+ "rating": 2,
+ "country_code": "ID",
+ "release_date": "2006-08-28",
+ "is_most_watched": true,
+ "created_at": "2022-10-30T14:45:02Z"
+ },
+ {
+ "id": 19,
+ "title": "Battle for Marjah, The",
+ "description": "tristique fusce congue diam id ornare imperdiet sapien urna pretium nisl ut volutpat sapien arcu sed augue aliquam erat volutpat in congue etiam justo",
+ "genre": "Documentary|War",
+ "rating": 7,
+ "country_code": "CN",
+ "release_date": "2003-08-06",
+ "is_most_watched": true,
+ "created_at": "2022-04-20T11:30:09Z"
+ },
+ {
+ "id": 20,
+ "title": "Purple Ball, The (Lilovyy shar)",
+ "description": "ante nulla justo aliquam quis turpis eget elit sodales scelerisque mauris sit amet eros suspendisse accumsan tortor quis turpis sed ante vivamus",
+ "genre": "Fantasy|Sci-Fi",
+ "rating": 6,
+ "country_code": "IE",
+ "release_date": "2003-04-23",
+ "is_most_watched": true,
+ "created_at": "2022-07-20T15:18:59Z"
+ },
+ {
+ "id": 21,
+ "title": "Black Girl (La noire de...)",
+ "description": "nam dui proin leo odio porttitor id consequat in consequat ut nulla sed accumsan felis ut at dolor quis odio consequat varius integer ac leo pellentesque ultrices mattis",
+ "genre": "Drama",
+ "rating": 10,
+ "country_code": "PL",
+ "release_date": "1955-12-22",
+ "is_most_watched": true,
+ "created_at": "2022-02-07T21:19:43Z"
+ },
+ {
+ "id": 22,
+ "title": "Day After Trinity, The",
+ "description": "convallis eget eleifend luctus ultricies eu nibh quisque id justo sit amet sapien dignissim vestibulum vestibulum ante ipsum",
+ "genre": "Documentary",
+ "rating": 10,
+ "country_code": "CN",
+ "release_date": "1977-12-02",
+ "is_most_watched": true,
+ "created_at": "2022-06-15T18:55:40Z"
+ },
+ {
+ "id": 23,
+ "title": "Two Arabian Knights",
+ "description": "semper porta volutpat quam pede lobortis ligula sit amet eleifend pede libero quis orci nullam molestie nibh in lectus pellentesque at nulla",
+ "genre": "Adventure|Comedy|Romance",
+ "rating": 6,
+ "country_code": "BR",
+ "release_date": "1980-01-02",
+ "is_most_watched": true,
+ "created_at": "2022-06-10T09:43:21Z"
+ },
+ {
+ "id": 24,
+ "title": "Scarlet Pimpernel, The",
+ "description": "amet consectetuer adipiscing elit proin interdum mauris non ligula pellentesque ultrices phasellus id sapien in sapien iaculis congue vivamus metus arcu adipiscing molestie hendrerit at vulputate vitae nisl",
+ "genre": "Action|Drama|Romance",
+ "rating": 3,
+ "country_code": "RU",
+ "release_date": "1964-10-21",
+ "is_most_watched": false,
+ "created_at": "2022-07-15T15:40:03Z"
+ },
+ {
+ "id": 25,
+ "title": "Hand, The (Ruka)",
+ "description": "lobortis ligula sit amet eleifend pede libero quis orci nullam molestie nibh in lectus pellentesque at nulla suspendisse potenti cras in purus eu magna",
+ "genre": "Animation",
+ "rating": 9,
+ "country_code": "BA",
+ "release_date": "2021-08-08",
+ "is_most_watched": false,
+ "created_at": "2022-12-17T10:53:29Z"
+ },
+ {
+ "id": 26,
+ "title": "Alexander's Ragtime Band",
+ "description": "nulla nunc purus phasellus in felis donec semper sapien a libero nam dui proin leo odio porttitor id consequat in consequat ut",
+ "genre": "Drama|Musical",
+ "rating": 7,
+ "country_code": "KM",
+ "release_date": "2019-12-18",
+ "is_most_watched": true,
+ "created_at": "2022-07-04T16:15:32Z"
+ },
+ {
+ "id": 27,
+ "title": "Unknown Soldier, The (Tuntematon sotilas)",
+ "description": "tincidunt in leo maecenas pulvinar lobortis est phasellus sit amet erat nulla tempus vivamus in felis eu",
+ "genre": "Drama|War",
+ "rating": 5,
+ "country_code": "UA",
+ "release_date": "1963-03-03",
+ "is_most_watched": false,
+ "created_at": "2022-06-25T19:10:22Z"
+ },
+ {
+ "id": 28,
+ "title": "Couch in New York, A",
+ "description": "auctor gravida sem praesent id massa id nisl venenatis lacinia aenean sit amet justo morbi ut odio cras",
+ "genre": "Comedy|Drama|Romance",
+ "rating": 1,
+ "country_code": "GR",
+ "release_date": "1961-10-18",
+ "is_most_watched": true,
+ "created_at": "2022-05-15T10:06:38Z"
+ },
+ {
+ "id": 29,
+ "title": "Stir of Echoes",
+ "description": "sollicitudin mi sit amet lobortis sapien sapien non mi integer ac neque duis bibendum morbi non quam nec dui luctus",
+ "genre": "Horror|Mystery|Thriller",
+ "rating": 8,
+ "country_code": "PH",
+ "release_date": "1990-12-02",
+ "is_most_watched": false,
+ "created_at": "2022-02-22T14:07:49Z"
+ },
+ {
+ "id": 30,
+ "title": "Amour fou, L'",
+ "description": "faucibus orci luctus et ultrices posuere cubilia curae duis faucibus accumsan odio curabitur convallis duis consequat dui nec nisi volutpat eleifend donec ut dolor morbi vel lectus in quam fringilla",
+ "genre": "Documentary",
+ "rating": 6,
+ "country_code": "ID",
+ "release_date": "1950-06-02",
+ "is_most_watched": true,
+ "created_at": "2022-05-05T11:16:13Z"
+ },
+ {
+ "id": 31,
+ "title": "Maze, The",
+ "description": "habitasse platea dictumst etiam faucibus cursus urna ut tellus nulla ut erat id mauris vulputate elementum nullam varius",
+ "genre": "Horror|Sci-Fi",
+ "rating": 1,
+ "country_code": "CZ",
+ "release_date": "2020-12-29",
+ "is_most_watched": true,
+ "created_at": "2022-11-05T20:41:18Z"
+ },
+ {
+ "title": "Solomon Northup's Odyssey",
+ "description": "ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae mauris viverra diam vitae quam suspendisse",
+ "genre": "(no genres listed)",
+ "rating": 4,
+ "country_code": "PE",
+ "release_date": "1966-08-09",
+ "is_most_watched": false,
+ "created_at": "2022-07-28T22:27:04Z",
+ "id": 32
+ },
+ {
+ "id": 33,
+ "title": "Scarecrow, The",
+ "description": "non sodales sed tincidunt eu felis fusce posuere felis sed lacus morbi sem mauris laoreet ut rhoncus aliquet pulvinar sed",
+ "genre": "Comedy",
+ "rating": 10,
+ "country_code": "JP",
+ "release_date": "2000-06-02",
+ "is_most_watched": false,
+ "created_at": "2022-02-19T17:52:47Z"
+ },
+ {
+ "id": 34,
+ "title": "Flying Deuces, The",
+ "description": "maecenas rhoncus aliquam lacus morbi quis tortor id nulla ultrices aliquet maecenas leo odio condimentum id luctus nec molestie sed justo pellentesque viverra pede ac diam cras pellentesque volutpat",
+ "genre": "Comedy",
+ "rating": 6,
+ "country_code": "PT",
+ "release_date": "2001-05-07",
+ "is_most_watched": true,
+ "created_at": "2022-04-16T15:25:02Z"
+ },
+ {
+ "id": 35,
+ "title": "Long Riders, The",
+ "description": "nisi venenatis tristique fusce congue diam id ornare imperdiet sapien urna pretium nisl ut volutpat sapien arcu",
+ "genre": "Western",
+ "rating": 2,
+ "country_code": "AL",
+ "release_date": "2006-08-10",
+ "is_most_watched": true,
+ "created_at": "2022-07-30T15:55:20Z"
+ },
+ {
+ "id": 36,
+ "title": "Chronicle of a Summer (Chronique d'un été)",
+ "description": "amet erat nulla tempus vivamus in felis eu sapien cursus vestibulum proin eu mi nulla ac enim in tempor turpis nec euismod scelerisque quam turpis adipiscing lorem vitae mattis",
+ "genre": "Documentary",
+ "rating": 7,
+ "country_code": "CN",
+ "release_date": "1993-10-02",
+ "is_most_watched": true,
+ "created_at": "2022-07-05T06:19:43Z"
+ },
+ {
+ "id": 37,
+ "title": "Harry Potter and the Goblet of Fire",
+ "description": "justo in hac habitasse platea dictumst etiam faucibus cursus urna ut tellus nulla ut erat id mauris vulputate elementum nullam varius nulla facilisi cras non velit",
+ "genre": "Adventure|Fantasy|Thriller|IMAX",
+ "rating": 4,
+ "country_code": "PH",
+ "release_date": "1974-02-14",
+ "is_most_watched": true,
+ "created_at": "2022-12-13T16:50:23Z"
+ },
+ {
+ "id": 38,
+ "title": "Karla",
+ "description": "lacus curabitur at ipsum ac tellus semper interdum mauris ullamcorper purus sit amet nulla quisque arcu libero rutrum ac lobortis vel dapibus",
+ "genre": "Crime|Drama|Thriller",
+ "rating": 2,
+ "country_code": "ID",
+ "release_date": "2011-05-19",
+ "is_most_watched": true,
+ "created_at": "2022-04-08T17:09:16Z"
+ },
+ {
+ "id": 39,
+ "title": "My Night At Maud's (Ma Nuit Chez Maud)",
+ "description": "ultrices posuere cubilia curae donec pharetra magna vestibulum aliquet ultrices erat tortor sollicitudin mi sit amet lobortis sapien sapien non mi integer ac neque duis",
+ "genre": "Comedy|Drama|Romance",
+ "rating": 3,
+ "country_code": "IR",
+ "release_date": "1957-09-19",
+ "is_most_watched": false,
+ "created_at": "2022-09-03T11:33:14Z"
+ },
+ {
+ "id": 40,
+ "title": "The Legend of Bloody Jack",
+ "description": "risus dapibus augue vel accumsan tellus nisi eu orci mauris lacinia sapien quis libero nullam sit amet turpis",
+ "genre": "Horror",
+ "rating": 1,
+ "country_code": "ID",
+ "release_date": "2000-01-19",
+ "is_most_watched": false,
+ "created_at": "2022-12-01T06:57:42Z"
+ },
+ {
+ "id": 41,
+ "title": "Beyond the Mat",
+ "description": "est congue elementum in hac habitasse platea dictumst morbi vestibulum velit id pretium iaculis diam erat fermentum justo nec condimentum neque sapien placerat ante nulla justo aliquam quis",
+ "genre": "Documentary",
+ "rating": 1,
+ "country_code": "CN",
+ "release_date": "1964-08-25",
+ "is_most_watched": true,
+ "created_at": "2022-03-18T04:12:33Z"
+ },
+ {
+ "id": 42,
+ "title": "Something Real and Good",
+ "description": "nullam sit amet turpis elementum ligula vehicula consequat morbi a ipsum integer a nibh in quis",
+ "genre": "Drama",
+ "rating": 10,
+ "country_code": "FR",
+ "release_date": "2008-04-07",
+ "is_most_watched": false,
+ "created_at": "2022-02-28T05:09:57Z"
+ },
+ {
+ "id": 43,
+ "title": "Combat dans L'Ile, Le (Fire and Ice)",
+ "description": "cras non velit nec nisi vulputate nonummy maecenas tincidunt lacus at velit vivamus vel nulla eget eros elementum pellentesque quisque porta volutpat erat",
+ "genre": "Drama",
+ "rating": 3,
+ "country_code": "KR",
+ "release_date": "1970-07-25",
+ "is_most_watched": true,
+ "created_at": "2022-06-24T08:12:54Z"
+ }
+ ],
+ "directors": [],
+ "genres": []
+}
diff --git a/example_projects/jsonserver-project/manage.py b/example_projects/jsonserver-project/manage.py
new file mode 100755
index 0000000..8005262
--- /dev/null
+++ b/example_projects/jsonserver-project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/example_projects/jsonserver-project/movies/__init__.py b/example_projects/jsonserver-project/movies/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example_projects/jsonserver-project/movies/admin.py b/example_projects/jsonserver-project/movies/admin.py
new file mode 100644
index 0000000..4c7055e
--- /dev/null
+++ b/example_projects/jsonserver-project/movies/admin.py
@@ -0,0 +1,22 @@
+from django.contrib import admin
+
+from .models import Movie
+
+
+class RestApiModelAdmin(admin.ModelAdmin):
+ list_display = ("id", "title", "genre", "rating", "is_most_watched")
+ readonly_fields = ("created_at",)
+ actions = None # Disable bulk actions as filtering is not implemented in RestAPI handler
+ list_per_page = 10
+
+ def save_model(self, request, obj, form, change):
+ obj.save(using="movie_collection_api")
+
+ def delete_model(self, request, obj):
+ obj.delete(using="movie_collection_api")
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).using("movie_collection_api")
+
+
+admin.site.register(Movie, RestApiModelAdmin)
diff --git a/example_projects/jsonserver-project/movies/apps.py b/example_projects/jsonserver-project/movies/apps.py
new file mode 100644
index 0000000..cced8a6
--- /dev/null
+++ b/example_projects/jsonserver-project/movies/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MoviesConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "movies"
diff --git a/example_projects/jsonserver-project/movies/migrations/__init__.py b/example_projects/jsonserver-project/movies/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example_projects/jsonserver-project/movies/models.py b/example_projects/jsonserver-project/movies/models.py
new file mode 100644
index 0000000..f40ced1
--- /dev/null
+++ b/example_projects/jsonserver-project/movies/models.py
@@ -0,0 +1,21 @@
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.utils import timezone
+
+
+class Movie(models.Model):
+ id = models.AutoField(primary_key=True) # type: ignore
+ title = models.CharField(blank=False, max_length=200) # type: ignore
+ description = models.TextField(blank=False, max_length=10000) # type: ignore
+ genre = models.CharField(blank=False, max_length=200) # type: ignore
+ rating = models.IntegerField(blank=False, validators=[MinValueValidator(0), MaxValueValidator(10)]) # type: ignore
+ country_code = models.CharField(blank=False, max_length=200) # type: ignore
+ release_date = models.DateField(
+ blank=False,
+ ) # type: ignore
+ is_most_watched = models.BooleanField(default=False) # type: ignore
+ created_at = models.DateTimeField(default=timezone.now) # type: ignore
+
+ class Meta:
+ db_table = "movies" # Used to determine API endpoint
+ managed = False
diff --git a/example_projects/jsonserver-project/movies/rest_handler.py b/example_projects/jsonserver-project/movies/rest_handler.py
new file mode 100644
index 0000000..b1d648f
--- /dev/null
+++ b/example_projects/jsonserver-project/movies/rest_handler.py
@@ -0,0 +1,120 @@
+import json
+from typing import Optional
+from urllib.parse import urlencode
+
+import requests
+from django.core.serializers.json import DjangoJSONEncoder
+from django.db.models.aggregates import Count
+
+from django_restapi_engine.rest_api_handler import BaseRestApiHandler
+
+
+class MovieCollectionApiHandler(BaseRestApiHandler):
+
+ BASE_URL = "http://localhost:3000"
+
+ def get_api_endpoint(self, model, pk=None):
+ """
+ Build API endpoint url based on model's `db_table`
+ """
+ if pk:
+ return f"{self.BASE_URL}/{model._meta.db_table}/{pk}"
+ return f"{self.BASE_URL}/{model._meta.db_table}"
+
+ def insert(self, *, model, obj, fields, returning_fields):
+
+ data = {}
+ for field in fields:
+ data[field.name] = getattr(obj, field.name)
+
+ r = requests.post(
+ self.get_api_endpoint(model),
+ data=json.dumps(data, cls=DjangoJSONEncoder),
+ headers={"content-type": "application/json"},
+ )
+ if r.status_code != 201:
+ raise ValueError("Unexpected response status code %s when inserting record", r.status_code)
+
+ row = r.json()
+
+ output = []
+ for field in returning_fields:
+ output.append(row[field.name])
+
+ return output
+
+ def get(self, *, model, pk, columns):
+ r = requests.get(self.get_api_endpoint(model, pk))
+ if r.status_code != 200:
+ raise ValueError("Unexpected response status code %s when fetching record", r.status_code)
+
+ row = r.json()
+ output = []
+ for col, _, _ in columns:
+ output.append(row[col.target.name])
+
+ return output
+
+ def update(self, *, model, pk, values) -> Optional[int]:
+ data = {}
+ for col, _, value in values:
+ data[col.name] = value
+
+ r = requests.put(
+ self.get_api_endpoint(model, pk),
+ data=json.dumps(data, cls=DjangoJSONEncoder),
+ headers={"content-type": "application/json"},
+ )
+ if r.status_code != 200:
+ raise ValueError("Unexpected response status code %s when updating record", r.status_code)
+
+ return 1
+
+ def delete(self, *, model, pk):
+ r = requests.delete(self.get_api_endpoint(model, pk))
+ if r.status_code != 200:
+ raise ValueError("Unexpected response status code %s when deleting record", r.status_code)
+
+ def list(self, *, model, columns, query):
+ """
+ Supported API features: https://github.com/typicode/json-server
+ """
+ if len(columns) == 1 and isinstance(columns[0][0], Count):
+ # Count
+ r = requests.get(self.get_api_endpoint(model))
+ if r.status_code != 200:
+ raise ValueError("Unexpected response status code %s when counting records", r.status_code)
+ return len(r.json())
+
+ def build_fetch_url():
+ """
+ Extract ordering and offset/limit from query and convert it to RestAPI params
+ """
+ sort = {}
+ for sort_column in query.order_by:
+ # Django prepends a minus (-) to a sorting column to indicate it's reverse ordering
+ api_column = sort_column.lstrip("-")
+ sort[api_column] = "desc" if sort_column[:1] == "-" else "asc"
+
+ params = {
+ "_sort": ",".join(sort.keys()),
+ "_order": ",".join(sort.values()),
+ "_start": query.low_mark,
+ "_end": query.high_mark,
+ }
+ clean_params = {k: v for k, v in params.items() if v} # Remove empty params
+ return f"{self.get_api_endpoint(model)}?{urlencode(clean_params)}"
+
+ r = requests.get(build_fetch_url())
+ if r.status_code != 200:
+ raise ValueError("Unexpected response status code %s when listing records", r.status_code)
+
+ rows = r.json()
+ output = []
+ for row in rows:
+ row_output = []
+ for col, _, _ in columns:
+ row_output.append(row[col.target.name])
+ output.append(row_output)
+
+ return output
diff --git a/example_projects/jsonserver-project/movies/urls.py b/example_projects/jsonserver-project/movies/urls.py
new file mode 100644
index 0000000..5119061
--- /dev/null
+++ b/example_projects/jsonserver-project/movies/urls.py
@@ -0,0 +1,7 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("", views.index, name="index"),
+]
diff --git a/example_projects/jsonserver-project/movies/views.py b/example_projects/jsonserver-project/movies/views.py
new file mode 100644
index 0000000..779e847
--- /dev/null
+++ b/example_projects/jsonserver-project/movies/views.py
@@ -0,0 +1,12 @@
+from html import escape
+
+from django.http import HttpResponse
+
+from .models import Movie
+
+
+def index(request):
+ out = ""
+ for movie in Movie.objects.using("movie_collection_api"):
+ out = f"{out}
{escape(movie.title)}"
+ return HttpResponse(f"")
diff --git a/example_projects/jsonserver-project/mysite/__init__.py b/example_projects/jsonserver-project/mysite/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example_projects/jsonserver-project/mysite/asgi.py b/example_projects/jsonserver-project/mysite/asgi.py
new file mode 100644
index 0000000..a2b4098
--- /dev/null
+++ b/example_projects/jsonserver-project/mysite/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for mysite project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
+
+application = get_asgi_application()
diff --git a/example_projects/jsonserver-project/mysite/fixtures/initial_data.yaml b/example_projects/jsonserver-project/mysite/fixtures/initial_data.yaml
new file mode 100644
index 0000000..26e42e1
--- /dev/null
+++ b/example_projects/jsonserver-project/mysite/fixtures/initial_data.yaml
@@ -0,0 +1,15 @@
+- fields:
+ date_joined: 2023-01-01 06:30:00+00:00
+ email: admin@example.com
+ first_name: 'Rest'
+ groups: []
+ is_active: true
+ is_staff: true
+ is_superuser: true
+ last_login: 2023-01-01 06:30:00+00:00
+ last_name: 'API'
+ password: pbkdf2_sha256$12000$scfDItXHnci7$ZloMPgY2YAhaFelKGqZ7eJB3mCYbxwua8TxNnCerwP8=
+ user_permissions: []
+ username: admin
+ model: auth.user
+ pk: 1
diff --git a/example_projects/jsonserver-project/mysite/settings.py b/example_projects/jsonserver-project/mysite/settings.py
new file mode 100644
index 0000000..b2dcc8c
--- /dev/null
+++ b/example_projects/jsonserver-project/mysite/settings.py
@@ -0,0 +1,130 @@
+"""
+Django settings for mysite project.
+
+Generated by 'django-admin startproject' using Django 3.2.9.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.2/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-fake-secret-key"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS: list[str] = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "movies.apps.MoviesConfig",
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "mysite.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "mysite.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ },
+ "movie_collection_api": {
+ "ENGINE": "django_restapi_engine",
+ "DEFAULT_HANDLER_CLASS": "movies.rest_handler.MovieCollectionApiHandler",
+ },
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.2/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.2/howto/static-files/
+
+STATIC_URL = "/static/"
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/example_projects/jsonserver-project/mysite/urls.py b/example_projects/jsonserver-project/mysite/urls.py
new file mode 100644
index 0000000..8396f5e
--- /dev/null
+++ b/example_projects/jsonserver-project/mysite/urls.py
@@ -0,0 +1,22 @@
+"""mysite URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.2/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import include, path
+
+urlpatterns = [
+ path("movies/", include("movies.urls")),
+ path("admin/", admin.site.urls),
+]
diff --git a/example_projects/jsonserver-project/mysite/wsgi.py b/example_projects/jsonserver-project/mysite/wsgi.py
new file mode 100644
index 0000000..4e33546
--- /dev/null
+++ b/example_projects/jsonserver-project/mysite/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for mysite project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
+
+application = get_wsgi_application()
diff --git a/example_projects/jsonserver-project/requirements.txt b/example_projects/jsonserver-project/requirements.txt
new file mode 100644
index 0000000..f77c248
--- /dev/null
+++ b/example_projects/jsonserver-project/requirements.txt
@@ -0,0 +1,4 @@
+-e ../../ # django-restapi-engine
+Django~=3.2
+PyYAML==6.*
+requests==2.*