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.*